code:
table.editorlayers {
	border-spacing: 4px 1px;
	border-collapse: separate;
}
.editorcolours > div {
	margin-right: 4em;
}
.editorcolours {
	display: flex;
	flex-wrap: wrap;
}
.editorcolours .export-block {
	float: right;
	padding-top: 1.33em;
}
.editorcolours table th {
	font-size: small;
	text-align: center;
}
input.editlayer-z, input.editlayer-alpha {
	width: 3.5em;
	min-width: unset;
}
input.editlayer-animation {
	width: 5em;
	min-width: unset;
}
input.editlayer-masksrc {
	width: 10em;
	min-width: unset;
}
input.editlayer-show {
	margin-left: 1em;
}
input.editlayer-desaturate {
	margin-left: 2.5em;
}
input.editlayer-brightness,
input.editlayer-contrast {
	min-width: unset;
	width: 3.5em;
}

.editormodelgroups {
	display: flex;
	justify-content: flex-start;
	align-items: flex-start;
}
.editormodelgroup {
	margin: 0 1em;
	display: grid;
	grid-template-columns: auto auto;
}
.editormodelgroup label {
	font-family: monospace;
}

.editormodelgroup .optioncategory {
	grid-column-start: 1;
	grid-column-end: 3;
	font-weight: bold;
	margin-top: 0.5em;
}
.editormodelgroup .optionlabel {
	grid-column-start: 1;
	align-self: center;
}
.editormodelgroup .optioneditor {
	grid-column-start: 2;
	justify-self: stretch;
	display: flex;
}
.optioneditor label {
	min-width: 2.75em;
}
.optioneditor input, .optioneditor select {
	flex-grow: 1;
}

repo_name: Area/degrees

license: mit


code:
.main-shop-div {
	position: relative;
}

.button-back-to-shop {
	position: absolute;
	top: -1em;
	left: min(1em, 2%);
	border: 1px solid #444;
	border-left: none;
	width: fit-content;
	padding: 0 0.5em;
	line-height: 2em;
}
.button-back-to-shop:before,
.button-back-to-shop:after {
	content: "";
	border-top: 1px solid #444;
	display: block;
	position: absolute;
	width: 1.16em;
	height: 1em;
	z-index: -1;
}
.button-back-to-shop:before {
	left: -1.16em;
	transform-origin: top right;
	transform: rotate(300deg);
}
.button-back-to-shop:after {
	left: 0;
	transform-origin: top left;
	transform: rotate(240deg);
}
.button-back-to-shop:hover,
.button-back-to-shop:hover::before,
.button-back-to-shop:hover::after {
	background-color: #222;
}

@media only screen and (max-width: 349px) {
	.button-back-to-shop {
		font-size: 90%;
	}
}
@media only screen and (max-width: 320px) {
	.button-back-to-shop {
		font-size: 80%;
	}
}

.clothing-shop-page {
	display: flex;
	flex-flow: wrap;
	justify-content: center;
}

.clothing-item {
	display: inline-flex;
	position: relative;
	width: 49%;
	min-width: 250px;
	border: 1px solid darkgrey;
	margin: 0.25%;
	transition: all 0.1s;
	cursor: pointer;
}
.clothing-item:hover {
	background-color: #222;
}

.clothing-item-spacer {
	display: inline-flex;
	width: 49%;
	min-width: 250px;
	height: 60px;
	border: 1px dashed dimgrey;
	margin: 0.25%;
}
.clothing-item-spacer.compact {
	height: 30px;
}

.clothing-icon {
	position: relative;
	display: flex;
	width: 60px;
	height: 60px;
}

.stats-ball {
	display: inline-block;
	width: 5px;
	height: 5px;
	margin-right: -3px;
	border: 1px solid #555;
	border-radius: 50%;
}

.warm-0 {
	background-color: #62c6ff;
}
.warm-1 {
	background-color: #9dfb76;
}
.warm-2 {
	background-color: #ffee3b;
}
.warm-3 {
	background-color: #ff793b;
}
.warm-4 {
	background-color: #ff481b;
}

.clothing-reveal,
.clothing-integrity,
.clothing-warmth {
	line-height: 50%;
	position: relative;
	top: -2px;
	left: 10px;
}

.clothing-stats-icon {
	width: 19px;
	height: 12px;
}

.clothing-stats-text {
	display: inline-block;
	vertical-align: top;
	font-size: xx-small;
	width: 12px;
	height: 12px;
}

.clothing-price {
	position: absolute;
	background: grey;
	color: black;
	font-size: 0.8em;
	font-weight: bold;
	border-radius: 50% 10% 10% 50%;
	padding: 0px 3px 0px 10px;
	transform: rotate(30deg);
	transform-origin: left;
	right: 1%;
	top: -12%;
	transition: all 0.2s;
}
.clothing-item:hover .clothing-price {
	background: gold;
	transform: rotate(10deg);
	filter: drop-shadow(2px 4px 6px black);
}
.clothing-price:before {
	display: inline-block;
	position: relative;
	background-color: black;
	width: 0.3em;
	height: 0.3em;
	border-radius: 50%;
	content: " ";
	vertical-align: middle;
	left: -15%;
}

.compact > .clothing-price {
	top: unset;
	transform: unset;
}

.pricetag-placeholder {
	width: 65px;
	height: 30px;
	float: right;
}

.clothing-icon img {
	image-rendering: -moz-crisp-edges;
	image-rendering: pixelated;
	transform-origin: top left;
	transform: scale(2, 2);
	height: 30px;
	width: 30px;
}
.clothing-icon #spanIcon #accIcon {
	right: initial;
	height: 30px;
}

.clothing-locked {
	position: absolute;
	width: 60px;
	height: 60px;
}

.clothing-icon.compact,
.clothing-icon.compact img {
	width: 30px;
	height: 30px;
	transform: unset;
	align-self: center;
}

.locked-hint {
	margin-left: 0.5em;
	line-height: 1.25em;
	margin-bottom: 0.25em;
	font-size: 0.9em;
	color: silver;
}
.unlocked-hint {
	font-size: 0.9em;
}

.clothing-name {
	position: relative;
	display: block;
	left: 10px;
	top: 0px;
	line-height: 1.1em;
	margin-top: 4px;
	margin-bottom: 4px;
	margin-right: 2em;
}

.clothing-body {
	width: 100%;
}
.clothing-body.compact {
	display: flex;
	align-items: center;
}

.clothing-gender {
	position: absolute;
	left: 44px;
	top: 42px;
	border-radius: 3px;
	width: 16px;
	height: 16px;
	font-size: 0.6em;
	font-weight: bold;
	text-align: center;
	line-height: 1.2em;
}

.clothing-gender > img,
.clothing-gender * {
	width: inherit;
	height: inherit;
	transform: unset;
}

.clothing-gender > span {
	font-size: 2em;
	font-weight: bolder;
	font-family: -webkit-pictograph;
}

.compact > .clothing-gender {
	left: 25px;
	top: 20px;
	width: 6px;
	height: 6px;
}
.compact > .clothing-gender > .male {
	background-color: #559BC0;
	border-radius: 3px;
}
.compact > .clothing-gender > .female {
	background-color: #E40081;
	border-radius: 3px;
}

.clothing-traits {
	display: inline-block;
	position: absolute;
	left: 165px;
	bottom: 10px;
	width: 100px;
	height: 20px;
	line-height: 1em;
}

.clothing-trait {
	width: 20px;
	height: 20px;
	display: inline-block;
	border: 1px solid lightgrey;
	border-radius: 10%;
	margin-right: -2px;
}

.compact > .clothing-traits {
	position: unset;
	left: unset;
	width: unset;
	white-space: nowrap;
}

.trait-description .clothing-trait {
	margin-right: 0.4em;
}

.trait-description {
	display: flex;
	flex-direction: row-reverse;
	place-content: flex-end;
	align-items: center;
}

@media only screen and (max-width: 639px) {
	.clothing-item,
	.clothing-item-spacer {
		min-width: 90%;
	}
	.clothing-name {
		font-size: 0.9em;
	}
}
@media only screen and (max-width: 577px) {
	.clothing-item,
	.clothing-item-spacer {
		min-width: 100%;
		margin: 0.5%;
	}
}
@media only screen and (max-width: 335px) {
	.clothing-reveal,
	.clothing-integrity,
	.clothing-warmth,
	.clothing-traits {
		display: none;
	}
	.clothing-icon {
		width: 30px;
		height: 30px;
	}
	.clothing-locked {
		width: 30px;
		height: 30px;
	}
	.clothing-item img {
		transform: unset;
	}
	.clothing-item-spacer {
		height: 30px;
	}
	.clothing-gender {
		left: 25px;
		top: 2px;
		width: 12px;
		height: 12px;
	}
	.clothing-item,
	.clothing-item-spacer {
		margin: 0.2em;
		align-items: center;
	}
	.clothing-price {
		background: none;
		color: gold;
		transform: rotate(0);
		font-size: 0.9em;
		top: 0;
	}
	.clothing-price:before {
		display: none;
	}
	.clothing-item:hover .clothing-price {
		background: none;
		transform: rotate(0);
		filter: none;
	}
}

/* Category tabs */
.shop-category-tabs {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	width: 96%;
	margin: 0 0 1em 0;
}

.category-group {
	display: inline-flex;
	margin: 0 5px;
}

.category-tab {
	-ms-transform: skewX(-30deg);
	-webkit-transform: skewX(-30deg);
	transform: skewX(-30deg);
	border: 1px solid white;
	margin: 0 0 -1px -1px;
	transition: all 0.1s;
}
.category-tab:hover {
	background-color: lightgrey;
	cursor: pointer;
}
.category-tab.active,
.category-tab.active:hover {
	background-color: white;
	cursor: unset;
}
.category-tab > img {
	-ms-transform: skewX(30deg);
	-webkit-transform: skewX(30deg);
	transform: skewX(30deg);
	padding: 0px 4px;
	transition: all 0.1s;
}
.category-tab.active > img,
.category-tab.active:hover > img {
	filter: invert(1);
}
.category-tab:hover > img {
	filter: invert(0.8);
}

.category-icon-alt {
	width: 30px;
	height: 30px;
	overflow: hidden;
	transform: skewX(30deg);
	display: flex;
	place-content: center;
	padding: 0 2px;
}
.active > .category-icon-alt,
.category-icon-alt:hover {
	color: black;
}

@media only screen and (max-width: 345px) {
	.category-group {
		margin: 0;
	}
	.category-tab > img {
		padding: 0 3px;
	}
}

/* Pagination */
.shop-pagination {
	display: flex;
	align-items: center;
	justify-content: center;
	margin: 1em 0 0 0;
}

.shop-pages {
	margin: 0 1em;
}
.shop-pages-number {
	margin: 0 0.5em;
}

.page {
	width: 0.75em;
	height: 0.75em;
	border: 3px solid white;
	border-radius: 50%;
	display: inline-flex;
	vertical-align: middle;
	transition: all 0.1s;
}
.page:not(.active) {
	cursor: pointer;
}
.page:hover {
	background-color: #222;
}
.page.active {
	background-color: white;
}
.page.active a {
	cursor: default;
}

.btn-pagination {
	border: 2px solid white;
	padding: 0 1em;
	text-align: center;
	cursor: pointer;
	transition: all 0.1s;
}
.btn-pagination:hover {
	background-color: #222;
}
.btn-pagination-arrow {
	font-family: monospace;
	font-size: xx-large;
	line-height: 0;
	display: none;
}

.div-link {
	cursor: pointer;
}
.div-link a {
	pointer-events: none;
}

@media only screen and (max-width: 577px) {
	.btn-pagination > a {
		display: none;
	}
	.btn-pagination {
		height: 2em;
		width: 0 !important;
		display: flex;
		justify-content: center;
		align-items: center;
	}
	.btn-pagination-arrow {
		display: block;
	}
}

/* Clothing details */

.clothing-details {
	margin-bottom: 1em;
	flex: auto;
}

.cold-resist-icon {
	background-image: url(img/misc/icon/coldresistance.png);
	background-repeat: no-repeat;
	background-size: contain;
	padding-left: 1.2em;
}

.colour-options-div {
	display: flex;
	flex-wrap: wrap;
}

.colour-button {
	border: 1px solid #444;
	margin: 0 0.25em 0.25em 0;
	background-color: #222;
	height: fit-content;
}
.colour-button:hover {
	/* mix-blend-mode: difference; */
	opacity: 80%;
}
.colour-button > div {
	display: flex;
	align-items: center;
	padding: 0px 0.3em;
}
.colour-button.active > .bg-custom {
	background-color: #F00;
}
.colour-button:not(.active) > .bg-custom {
	filter: none !important;
}
.colour-options-div > .active > div {
	box-shadow: inset 0 0 0 1px #FFF, inset 0 0 0 2px #000;
}

.colour-button > div > img {
	height: 1.4em;
	margin-right: 0.3em;
	mix-blend-mode: exclusion;
}

.colour-name-span {
	color: #444;
	mix-blend-mode: hard-light;
	filter: drop-shadow(-1px -1px 0px #AAA) drop-shadow(-1px 1px 0px #AAA) drop-shadow(1px 1px 0px #AAA) drop-shadow(1px -1px 0px #AAA);
	pointer-events: none;
}
.colour-name-span-highcontrast {
	color: #111;
	mix-blend-mode: normal;
	filter: drop-shadow(1px 1px 0px #DDD) drop-shadow(1px -1px 0px #DDD) drop-shadow(-1px -1px 0px #DDD) drop-shadow(-1px 1px 0px #DDD);
	pointer-events: none;
}

@media only screen and (max-width: 490px) {
	.colour-options-div {
		font-size: 0.9em;
	}
}
@media only screen and (max-width: 380px) {
	.colour-options-div {
		font-size: 0.8em;
	}
}

/* Mannequin */

.mannequin,
.mannequin-placeholder {
	width: 85px;
	height: 200px;
	margin-right: 1em;
	margin-bottom: 1em;
	flex-shrink: 0;
}
.mannequin.both-renderers {
	width: 190px;
}
.mannequin {
	overflow: hidden;
	border: 1px solid #444;
	background-color: #222;
	display: table;
	z-index: 1;
}
.mannequin-placeholder {
	float: left;
}

.mannequin-inner {
	transform: translateX(-75px);
}
.canvas-mannequin {
	position: absolute;
}
.both-renderers .canvas-mannequin {
	transform: translateX(85px);
}

.mannequin img {
	position: absolute;
}

.clothing-colours-div {
	position: relative;
	display: flex;
	margin-bottom: 1em;
}

.mannequin-buttons {
	position: absolute;
	top: 2px;
	right: -14px;
	z-index: 1;
}

.mannequin-gender-button {
	width: 16px;
	height: 16px;
	border-radius: 20%;
	padding: 2px;
	transition: all 0.1s;
	opacity: 20%;
}
.mannequin-gender-button:hover {
	background-color: #6666;
	opacity: 100%;
}

.mannequin-gender-button > img {
	position: absolute;
	width: inherit;
	height: inherit;
}
.mannequin-gender-button > span {
	display: block;
	line-height: 1em;
	text-align: center;
}

.mannequin-gender-textbutton {
	width: fit-content;
	border-radius: 2px;
	padding: 0 7px;
	transition: all 0.1s;
	font-size: 0.7em;
	position: absolute;
	line-height: 1.2;
	top: calc(200px + 0.4em);
	left: 0;
	color: #6688dd;
}
.mannequin-gender-textbutton:hover {
	background-color: #222;
}
.mannequin-gender-textbutton:hover:before {
	content: "[";
	position: absolute;
	left: 0;
}
.mannequin-gender-textbutton:hover:after {
	content: "]";
	position: absolute;
	right: 0;
}

/* Custom colour sliders */

.colours-container {
	width: 100%;
	min-width: 100%;
}
.translate-colours-container {
	transform: translateX(calc(-85px - 1em));
}

.custom-colour {
	display: flex;
}

.custom-colour-sliders {
	display: flex;
	flex-flow: wrap;
	justify-content: space-around;
}

.colour-slider {
	width: 48%;
	font-size: 0.8em;
}

.colour-slider > .macro-numberslider {
	display: flex;
}

.colour-slider > .macro-numberslider > input {
	border-radius: 3px 0 0 3px;
}

.colour-slider > .macro-numberslider > span {
	border: 1px solid #444;
	border-left: none;
	margin-left: 0;
	border-radius: 0 3px 3px 0;
	line-height: 200%;
}

@media only screen and (max-width: 600px) {
	.custom-colour {
		flex-wrap: wrap;
	}
}
@media only screen and (max-width: 490px) {
	.colour-slider {
		display: flex;
		width: 100%;
	}
	.colour-slider-caption {
		width: max(30%, 7em);
		margin-right: 1em;
	}
}

/* Custom colour presets */

.custom-colour-presets {
	display: flex;
	flex-direction: column;
	line-height: 80%;
	margin-left: 1em;
	place-content: space-evenly;
	padding-top: 1.2em;
}

.presets-dropdown {
	display: flex;
	width: max-content;
}

.save-preset {
	display: flex;
	justify-content: center;
	align-items: center;
	width: 100%;
	height: 30px;
	border: 1px solid #444;
	border-radius: 4px;
	transition: all 0.1s;
}
.save-preset > img {
	height: 24px;
	width: 24px;
	place-self: center;
}

.delete-preset {
	display: flex;
	justify-content: center;
	align-items: center;
	margin-left: 0.5em;
	width: 24px;
	height: 24px;
	padding: 3px;
	border: 1px solid #444;
	border-radius: 3px;
	transition: all 0.1s;
}
.delete-preset > img {
	width: inherit;
	height: inherit;
}

.save-preset:hover,
.delete-preset:hover {
	background-color: #222;
}

@media only screen and (max-width: 600px) {
	.custom-colour-presets {
		flex-direction: row;
		padding-top: 0.5em;
		margin-left: 0.25em;
	}
	.save-preset {
		width: 100px;
		margin-left: 1em;
	}
}

#sendItemsTo .returnOptions {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	margin: 1em 0;
}

#sendItemsTo .returnOptions div {
	width: 10em;
}

/* Buy & Try on buttons */

.buy-buttons,
.try-buttons {
	display: flex;
	flex-wrap: wrap;
	margin: 1em 0;
}

#buy-send-home {
	width: 19em;
}

.buy-button,
.try-button {
	display: flex;
	flex-direction: column;
	margin-right: 1em;
	margin-bottom: 0.5em;
	border: 1px solid #444;
	line-height: 2em;
}
.buy-button:last-child,
.try-button:last-child {
	margin-right: 0;
}
.try-button {
	padding: 0 0.5em;
}

.buy-button-inner {
	padding: 0 0.5em;
	height: 100%;
	display: flex;
	align-items: center;
	justify-content: space-evenly;
}
.buy-button-inner > a {
	margin-right: 0.5em;
}

.buy-button-inner:hover,
.try-button:hover {
	background-color: #222;
}

.disabled {
	border-color: #333;
	color: #444;
	cursor: default;
	background: unset !important;
}
.disabled * {
	color: #444;
}
.disabled img {
	filter: saturate(0) brightness(0.4);
}

.buy-multiple {
	border-right: none;
}

/*
.buy-multiple-slider > div {
	display: flex;
	flex-direction: row-reverse;
}
*/

.buy-multiple-slider > div > input {
	border: none;
	height: 100%;
	max-width: unset;
}

.buy-multiple-slider > div > span {
	display: none;
}

@media only screen and (max-width: 380px) {
	.buy-buttons, .try-buttons {
		font-size: 0.9em;
	}
}
@media only screen and (max-width: 335px) {
	.buy-buttons, .try-buttons {
		font-size: 0.8em;
	}
}

/* Filters */
.filters-button,
.clothingshop-options-button,
.shop-legend-button {
	position: absolute;
	width: 48px;
	height: 48px;
	top: -1.5em;
	border: 1px solid #444;
	border-radius: 3px;
	transition: all 0.1s;
	display: flex;
	justify-content: center;
	align-items: center;
}
.filters-button {
	right: 1em;
}
.clothingshop-options-button {
	right: calc(1.6em + 50px);
}
.shop-legend-button {
	right: calc(2.2em + 100px);
}
.filters-button:hover,
.clothingshop-options-button:hover,
.shop-legend-button:hover,
.shop-legend-button.active {
	background-color: #222;
}

@media only screen and (max-width: 420px) {
	.filters-button,
	.clothingshop-options-button,
	.shop-legend-button {
		zoom: 0.75;
	}
	.filters-button {
		right: 0;
	}
	.clothingshop-options-button {
		right: calc(0.6em + 50px);
	}
	.shop-legend-button {
		right: calc(1.2em + 100px);
	}
}
@media only screen and (max-width: 335px) {
	.filters-button,
	.clothingshop-options-button,
	.shop-legend-button {
		zoom: 0.65;
	}
	.filters-button {
		right: 0;
	}
	.clothingshop-options-button {
		right: calc(0.6em + 50px);
	}
	.shop-legend-button {
		right: calc(1.2em + 100px);
	}
}

.button-icon,
.button-icon > img {
	width: 32px;
	height: 32px;
}

.no-click-overlay {
	position: fixed;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	pointer-events: all;
	backdrop-filter: blur(3px) brightness(0.5);
	z-index: 999;
}

.filters-div,
.options-div {
	position: fixed;
	left: min(7%, 8em);
	right: min(7%, 8em);
	top: min(5%, 8em);
	bottom: min(5%, 8em);
	border: 2px solid white;
	background: #111;
	padding: 1em;
	display: flex;
	flex-direction: column;
}

.filter-buttons-div,
.options-buttons-div {
	display: flex;
	flex-direction: row-reverse;
	flex-wrap: wrap;
	padding-bottom: 0.5em;
	border-bottom: 2px solid #444;
}

.filters {
	display: flex;
	flex-wrap: wrap;
	overflow: auto;
}

.options-body {
	flex-wrap: wrap;
	overflow: auto;
}

.filter-block {
	display: flex;
	flex-direction: column;
	border: 1px dashed #444;
	padding: 0.5em 1em;
	margin: 0.5em;
}

.filter-reveal-fromto,
.filter-warmth-fromto {
	display: flex;
	flex-direction: column;
	place-items: flex-end;
}
.filter-reveal-fromto > label,
.filter-warmth-fromto > label {
	margin-bottom: 0.5em;
}
.filter-warmth-fromto input {
	width: 7em;
}

.traits-list {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	justify-content: space-between;
}

.filters-trait-row {
	display: flex;
	align-items: center;
	margin-bottom: 0.4em;
	padding-left: 0.5em;
	width: 24%;
	min-width: 10em;
}
.filters-trait-row > label {
	display: flex;
	align-items: center;
}
.filters-trait-row > label > * {
	margin-right: 0.5em;
}

.filter-button,
.options-button {
	border: 1px solid #444;
	text-align: center;
	padding: 0 1em;
	margin: 0.25em 1em;
}
.filter-button:hover,
.options-button:hover {
	background-color: #222;
}

.high-contrast-example {
	display: flex;
	flex-wrap: wrap;
}

/* Legend */
.shop-legend {
	position: absolute;
	top: 2em;
	border: 1px solid;
	background: #222;
	padding: 1em;
	font-size: 90%;
	right: 0%;
	left: 0%;
	z-index: 100;
}

.legend-caption {
	text-align: center;
	font-size: 130%;
}

.legend-2em {
	display: flex;
	align-items: center;
	margin-bottom: 0.5em;
}
.legend-2em img {
	height: 1.5em;
	margin-right: 0.5em;
}

.legend-warmth-div {
	display: inline-flex;
	width: 40%;
	vertical-align: super;
	margin-right: 0.5em;
}

.legend-colour-sample {
	display: inline-flex;
	width: 1em;
	height: 0.5em;
	border-radius: 3px;
}

.legend-icon {
	width: 1em;
	display: inline-flex;
	justify-content: center;
	vertical-align: middle;
}

/* Warmth scale */
.warmth-scale-container {
	position: relative;
	width: 100%;
	height: 24px;
	overflow: hidden;
}
.warmth-scale-div {
	width: 100%;
	height: 5px;
	display: flex;
}
.warmth-scale-div div {
	height: 100%;
	transition: all 0.2s;
}
.warmth-scale-div > .cold {
	border-radius: 2px 0 0 2px;
	background-color: dodgerblue;
	box-shadow: -10px 0px 5px -3px inset lightskyblue;
}
.warmth-scale-div > .chill {
	background-color: lightskyblue;
	box-shadow: -10px 0px 5px -3px inset lemonchiffon;
}
.warmth-scale-div > .chill.nocold {
	border-radius: 2px 0 0 2px;
}
.warmth-scale-div > .ok {
	background-color: lemonchiffon;
	box-shadow: -10px 0px 5px -3px inset orange;
}
.warmth-scale-div > .ok.nowarm {
	border-radius: 0 2px 2px 0;
	box-shadow: unset;
}
.warmth-scale-div > .warm {
	background-color: orange;
	box-shadow: -10px 0px 5px -3px inset orangered;
}
.warmth-scale-div > .warm.nohot {
	border-radius: 0 2px 2px 0;
	box-shadow: unset;
}
.warmth-scale-div > .hot {
	border-radius: 0 2px 2px 0;
	background-color: orangered;
}

.warmth-scale-div > .diff {
	position: absolute;
	height: 5px;
	mix-blend-mode: luminosity;
}
.warmth-scale-div > .diff.up {
	background: linear-gradient(90deg, transparent, #EF0000D0);
}
.warmth-scale-div > .diff.down {
	background: linear-gradient(90deg, #008EEFD0, transparent);
}

.warmth-scale-container > .warmth-indicator {
	position: absolute;
	transform: translateX(-50%);
}

#warmth-description {
	font-size: 0.9em;
}


code:
.debug-images {
	height: 256px;
	width: 120px;
	overflow: hidden;
	background-color: #222;
	border: solid 1px #ddd;
}
.debug-images > * {
	position: absolute;
	height: 256px;
	overflow: hidden;
	left: -57px;
}

.debug-hideImageOverflow {
	overflow: hidden;
	height: 256px;
	width: 120px;
	position: relative;
}

.left {
	float: left;
}

.debugPassages {
	max-width: 2000px !important;
}

#saveCompareUI {
	border: solid 1px #aaa;
}

#saveCompareUI {
	border: solid 1px #aaa;
}

#comparedResult .item {
	border: solid 1px #aaa;
	padding: 3px;
	margin: 3px 0;
	word-wrap: break-word;
}

.debug-event-container {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
}

.debug-event-item {
    margin: 0.15rem;
	padding: 0.15rem;
	min-height: 6rem;
    flex-basis: 16rem;
    flex-grow: 1;
}

.debug-event-empty {
	background: repeating-linear-gradient(
		45deg,
		#222,
		#222 10px,
		#444 10px,
		#444 20px
	);
	color: gray;
}

.debug-event-success {
    border: solid 1px rgba(50, 200, 50, 0.5);
}

.debug-event-warning {
    border: solid 1px rgba(200, 200, 50, 0.5);
}

.debug-event-error {
    border: solid 1px rgba(200, 50, 50, 0.5);
}

.debug-event-item hr {
	margin: 0rem;
}


code:
/**
Ion.RangeSlider, 2.3.1
© Denis Ineshin, 2010 - 2019, IonDen.com
Build date: 2019-12-19 16:51:02
*/
.irs {
  position: relative;
  display: block;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  font-size: 12px;
  font-family: Arial, sans-serif;
}
.irs-line {
  position: relative;
  display: block;
  overflow: hidden;
  outline: none !important;
}
.irs-bar {
  position: absolute;
  display: block;
  left: 0;
  width: 0;
}
.irs-shadow {
  position: absolute;
  display: none;
  left: 0;
  width: 0;
}
.irs-handle {
  position: absolute;
  display: block;
  box-sizing: border-box;
  cursor: default;
  z-index: 1;
}
.irs-handle.type_last {
  z-index: 2;
}
.irs-min,
.irs-max {
  position: absolute;
  display: block;
  cursor: default;
}
.irs-min {
  left: 0;
}
.irs-max {
  right: 0;
}
.irs-from,
.irs-to,
.irs-single {
  position: absolute;
  display: block;
  top: 0;
  left: 0;
  cursor: default;
  white-space: nowrap;
}
.irs-grid {
  position: absolute;
  display: none;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 20px;
}
.irs-with-grid .irs-grid {
  display: block;
}
.irs-grid-pol {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 8px;
  background: #000;
}
.irs-grid-pol.small {
  height: 4px;
}
.irs-grid-text {
  position: absolute;
  bottom: 0;
  left: 0;
  white-space: nowrap;
  text-align: center;
  font-size: 9px;
  line-height: 9px;
  padding: 0 3px;
  color: #000;
}
.irs-disable-mask {
  position: absolute;
  display: block;
  top: 0;
  left: -1%;
  width: 102%;
  height: 100%;
  cursor: default;
  background: rgba(0, 0, 0, 0);
  z-index: 2;
}
.lt-ie9 .irs-disable-mask {
  background: #000;
  filter: alpha(opacity=0);
  cursor: not-allowed;
}
.irs-disabled {
  opacity: 0.4;
}
.irs-hidden-input {
  position: absolute !important;
  display: block !important;
  top: 0 !important;
  left: 0 !important;
  width: 0 !important;
  height: 0 !important;
  font-size: 0 !important;
  line-height: 0 !important;
  padding: 0 !important;
  margin: 0 !important;
  overflow: hidden;
  outline: none !important;
  z-index: -9999 !important;
  background: none !important;
  border-style: solid !important;
  border-color: transparent !important;
}
.irs--flat {
  height: 40px;
}
.irs--flat.irs-with-grid {
  height: 60px;
}
.irs--flat .irs-line {
  top: 25px;
  height: 12px;
  background-color: #e1e4e9;
  border-radius: 4px;
}
.irs--flat .irs-bar {
  top: 25px;
  height: 12px;
  background-color: #ed5565;
}
.irs--flat .irs-bar--single {
  border-radius: 4px 0 0 4px;
}
.irs--flat .irs-shadow {
  height: 1px;
  bottom: 16px;
  background-color: #e1e4e9;
}
.irs--flat .irs-handle {
  top: 22px;
  width: 16px;
  height: 18px;
  background-color: transparent;
}
.irs--flat .irs-handle > i:first-child {
  position: absolute;
  display: block;
  top: 0;
  left: 50%;
  width: 2px;
  height: 100%;
  margin-left: -1px;
  background-color: #da4453;
}
.irs--flat .irs-handle.state_hover > i:first-child,
.irs--flat .irs-handle:hover > i:first-child {
  background-color: #a43540;
}
.irs--flat .irs-min,
.irs--flat .irs-max {
  top: 0;
  padding: 1px 3px;
  color: #999;
  font-size: 10px;
  line-height: 1.333;
  text-shadow: none;
  background-color: #e1e4e9;
  border-radius: 4px;
}
.irs--flat .irs-from,
.irs--flat .irs-to,
.irs--flat .irs-single {
  color: white;
  font-size: 10px;
  line-height: 1.333;
  text-shadow: none;
  padding: 1px 5px;
  background-color: #ed5565;
  border-radius: 4px;
}
.irs--flat .irs-from:before,
.irs--flat .irs-to:before,
.irs--flat .irs-single:before {
  position: absolute;
  display: block;
  content: "";
  bottom: -6px;
  left: 50%;
  width: 0;
  height: 0;
  margin-left: -3px;
  overflow: hidden;
  border: 3px solid transparent;
  border-top-color: #ed5565;
}
.irs--flat .irs-grid-pol {
  background-color: #e1e4e9;
}
.irs--flat .irs-grid-text {
  color: #999;
}
.irs--big {
  height: 55px;
}
.irs--big.irs-with-grid {
  height: 70px;
}
.irs--big .irs-line {
  top: 33px;
  height: 12px;
  background-color: white;
  background: linear-gradient(to bottom, #ddd -50%, white 150%);
  border: 1px solid #ccc;
  border-radius: 12px;
}
.irs--big .irs-bar {
  top: 33px;
  height: 12px;
  background-color: #92bce0;
  border: 1px solid #428bca;
  background: linear-gradient(to bottom, #ffffff 0%, #428bca 30%, #b9d4ec 100%);
  box-shadow: inset 0 0 1px 1px rgba(255, 255, 255, 0.5);
}
.irs--big .irs-bar--single {
  border-radius: 12px 0 0 12px;
}
.irs--big .irs-shadow {
  height: 1px;
  bottom: 16px;
  background-color: rgba(66, 139, 202, 0.5);
}
.irs--big .irs-handle {
  top: 25px;
  width: 30px;
  height: 30px;
  border: 1px solid rgba(0, 0, 0, 0.3);
  background-color: #cbcfd5;
  background: linear-gradient(to bottom, white 0%, #B4B9BE 30%, white 100%);
  box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2), inset 0 0 3px 1px white;
  border-radius: 30px;
}
.irs--big .irs-handle.state_hover,
.irs--big .irs-handle:hover {
  border-color: rgba(0, 0, 0, 0.45);
  background-color: #939ba7;
  background: linear-gradient(to bottom, white 0%, #919BA5 30%, white 100%);
}
.irs--big .irs-min,
.irs--big .irs-max {
  top: 0;
  padding: 1px 5px;
  color: white;
  text-shadow: none;
  background-color: #9f9f9f;
  border-radius: 3px;
}
.irs--big .irs-from,
.irs--big .irs-to,
.irs--big .irs-single {
  color: white;
  text-shadow: none;
  padding: 1px 5px;
  background-color: #428bca;
  background: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
  border-radius: 3px;
}
.irs--big .irs-grid-pol {
  background-color: #428bca;
}
.irs--big .irs-grid-text {
  color: #428bca;
}
.irs--modern {
  height: 55px;
}
.irs--modern.irs-with-grid {
  height: 55px;
}
.irs--modern .irs-line {
  top: 25px;
  height: 5px;
  background-color: #d1d6e0;
  background: linear-gradient(to bottom, #e0e4ea 0%, #d1d6e0 100%);
  border: 1px solid #a3adc1;
  border-bottom-width: 0;
  border-radius: 5px;
}
.irs--modern .irs-bar {
  top: 25px;
  height: 5px;
  background: #20b426;
  background: linear-gradient(to bottom, #20b426 0%, #18891d 100%);
}
.irs--modern .irs-bar--single {
  border-radius: 5px 0 0 5px;
}
.irs--modern .irs-shadow {
  height: 1px;
  bottom: 21px;
  background-color: rgba(209, 214, 224, 0.5);
}
.irs--modern .irs-handle {
  top: 37px;
  width: 12px;
  height: 13px;
  border: 1px solid #a3adc1;
  border-top-width: 0;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
  border-radius: 0 0 3px 3px;
}
.irs--modern .irs-handle > i:nth-child(1) {
  position: absolute;
  display: block;
  top: -4px;
  left: 1px;
  width: 6px;
  height: 6px;
  border: 1px solid #a3adc1;
  background: white;
  transform: rotate(45deg);
}
.irs--modern .irs-handle > i:nth-child(2) {
  position: absolute;
  display: block;
  box-sizing: border-box;
  top: 0;
  left: 0;
  width: 10px;
  height: 12px;
  background: #e9e6e6;
  background: linear-gradient(to bottom, white 0%, #e9e6e6 100%);
  border-radius: 0 0 3px 3px;
}
.irs--modern .irs-handle > i:nth-child(3) {
  position: absolute;
  display: block;
  box-sizing: border-box;
  top: 3px;
  left: 3px;
  width: 4px;
  height: 5px;
  border-left: 1px solid #a3adc1;
  border-right: 1px solid #a3adc1;
}
.irs--modern .irs-handle.state_hover,
.irs--modern .irs-handle:hover {
  border-color: #7685a2;
  background: #c3c7cd;
  background: linear-gradient(to bottom, #ffffff 0%, #919ba5 30%, #ffffff 100%);
}
.irs--modern .irs-handle.state_hover > i:nth-child(1),
.irs--modern .irs-handle:hover > i:nth-child(1) {
  border-color: #7685a2;
}
.irs--modern .irs-handle.state_hover > i:nth-child(3),
.irs--modern .irs-handle:hover > i:nth-child(3) {
  border-color: #48536a;
}
.irs--modern .irs-min,
.irs--modern .irs-max {
  top: 0;
  font-size: 10px;
  line-height: 1.333;
  text-shadow: none;
  padding: 1px 5px;
  color: white;
  background-color: #d1d6e0;
  border-radius: 5px;
}
.irs--modern .irs-from,
.irs--modern .irs-to,
.irs--modern .irs-single {
  font-size: 10px;
  line-height: 1.333;
  text-shadow: none;
  padding: 1px 5px;
  background-color: #20b426;
  color: white;
  border-radius: 5px;
}
.irs--modern .irs-from:before,
.irs--modern .irs-to:before,
.irs--modern .irs-single:before {
  position: absolute;
  display: block;
  content: "";
  bottom: -6px;
  left: 50%;
  width: 0;
  height: 0;
  margin-left: -3px;
  overflow: hidden;
  border: 3px solid transparent;
  border-top-color: #20b426;
}
.irs--modern .irs-grid {
  height: 25px;
}
.irs--modern .irs-grid-pol {
  background-color: #dedede;
}
.irs--modern .irs-grid-text {
  color: silver;
  font-size: 13px;
}
.irs--sharp {
  height: 50px;
  font-size: 12px;
  line-height: 1;
}
.irs--sharp.irs-with-grid {
  height: 57px;
}
.irs--sharp .irs-line {
  top: 30px;
  height: 2px;
  background-color: black;
  border-radius: 2px;
}
.irs--sharp .irs-bar {
  top: 30px;
  height: 2px;
  background-color: #ee22fa;
}
.irs--sharp .irs-bar--single {
  border-radius: 2px 0 0 2px;
}
.irs--sharp .irs-shadow {
  height: 1px;
  bottom: 21px;
  background-color: rgba(0, 0, 0, 0.5);
}
.irs--sharp .irs-handle {
  top: 25px;
  width: 10px;
  height: 10px;
  background-color: #a804b2;
}
.irs--sharp .irs-handle > i:first-child {
  position: absolute;
  display: block;
  top: 100%;
  left: 0;
  width: 0;
  height: 0;
  border: 5px solid transparent;
  border-top-color: #a804b2;
}
.irs--sharp .irs-handle.state_hover,
.irs--sharp .irs-handle:hover {
  background-color: black;
}
.irs--sharp .irs-handle.state_hover > i:first-child,
.irs--sharp .irs-handle:hover > i:first-child {
  border-top-color: black;
}
.irs--sharp .irs-min,
.irs--sharp .irs-max {
  color: white;
  font-size: 14px;
  line-height: 1;
  top: 0;
  padding: 3px 4px;
  opacity: 0.4;
  background-color: #a804b2;
  border-radius: 2px;
}
.irs--sharp .irs-from,
.irs--sharp .irs-to,
.irs--sharp .irs-single {
  font-size: 14px;
  line-height: 1;
  text-shadow: none;
  padding: 3px 4px;
  background-color: #a804b2;
  color: white;
  border-radius: 2px;
}
.irs--sharp .irs-from:before,
.irs--sharp .irs-to:before,
.irs--sharp .irs-single:before {
  position: absolute;
  display: block;
  content: "";
  bottom: -6px;
  left: 50%;
  width: 0;
  height: 0;
  margin-left: -3px;
  overflow: hidden;
  border: 3px solid transparent;
  border-top-color: #a804b2;
}
.irs--sharp .irs-grid {
  height: 25px;
}
.irs--sharp .irs-grid-pol {
  background-color: #dedede;
}
.irs--sharp .irs-grid-text {
  color: silver;
  font-size: 13px;
}
.irs--round {
  height: 50px;
}
.irs--round.irs-with-grid {
  height: 65px;
}
.irs--round .irs-line {
  top: 36px;
  height: 4px;
  background-color: #dee4ec;
  border-radius: 4px;
}
.irs--round .irs-bar {
  top: 36px;
  height: 4px;
  background-color: #006cfa;
}
.irs--round .irs-bar--single {
  border-radius: 4px 0 0 4px;
}
.irs--round .irs-shadow {
  height: 4px;
  bottom: 21px;
  background-color: rgba(222, 228, 236, 0.8);
}
.irs--round .irs-handle {
  top: 26px;
  width: 24px;
  height: 24px;
  border: 4px solid #006cfa;
  background-color: white;
  border-radius: 24px;
  box-shadow: 0 1px 3px rgba(0, 0, 255, 0.3);
}
.irs--round .irs-handle.state_hover,
.irs--round .irs-handle:hover {
  background-color: #f0f6ff;
}
.irs--round .irs-min,
.irs--round .irs-max {
  color: #333;
  font-size: 14px;
  line-height: 1;
  top: 0;
  padding: 3px 5px;
  background-color: rgba(255, 255, 255, 0.8);
  border-radius: 4px;
}
.irs--round .irs-from,
.irs--round .irs-to,
.irs--round .irs-single {
  font-size: 14px;
  line-height: 1;
  text-shadow: none;
  padding: 3px 5px;
  background-color: #006cfa;
  color: white;
  border-radius: 4px;
}
.irs--round .irs-from:before,
.irs--round .irs-to:before,
.irs--round .irs-single:before {
  position: absolute;
  display: block;
  content: "";
  bottom: -6px;
  left: 50%;
  width: 0;
  height: 0;
  margin-left: -3px;
  overflow: hidden;
  border: 3px solid transparent;
  border-top-color: #006cfa;
}
.irs--round .irs-grid {
  height: 25px;
}
.irs--round .irs-grid-pol {
  background-color: #dedede;
}
.irs--round .irs-grid-text {
  color: silver;
  font-size: 13px;
}
.irs--square {
  height: 50px;
}
.irs--square.irs-with-grid {
  height: 60px;
}
.irs--square .irs-line {
  top: 31px;
  height: 4px;
  background-color: #dedede;
}
.irs--square .irs-bar {
  top: 31px;
  height: 4px;
  background-color: black;
}
.irs--square .irs-shadow {
  height: 2px;
  bottom: 21px;
  background-color: #dedede;
}
.irs--square .irs-handle {
  top: 25px;
  width: 16px;
  height: 16px;
  border: 3px solid black;
  background-color: white;
  -webkit-transform: rotate(45deg);
  -ms-transform: rotate(45deg);
  transform: rotate(45deg);
}
.irs--square .irs-handle.state_hover,
.irs--square .irs-handle:hover {
  background-color: #f0f6ff;
}
.irs--square .irs-min,
.irs--square .irs-max {
  color: #333;
  font-size: 14px;
  line-height: 1;
  top: 0;
  padding: 3px 5px;
  background-color: rgba(0, 0, 0, 0.1);
}

.irs--square .irs-single {
  font-size: 14px;
  line-height: 1;
  text-shadow: none;
  padding: 3px 5px;
  background-color: black;
  color: white;
}

.irs--square .irs-from {
  font-size: 14px;
  line-height: 1;
  text-shadow: none;
  padding: 3px 5px;
  background-color: black;
  color: rgb(0, 0, 255);
}

.irs--square .irs-to {
  font-size: 14px;
  line-height: 1;
  text-shadow: none;
  padding: 3px 5px;
  background-color: black;
  color: rgb(255, 0, 0);
}

.irs--square .irs-grid {
  height: 25px;
}
.irs--square .irs-grid-pol {
  background-color: #dedede;
}
.irs--square .irs-grid-text {
  color: silver;
  font-size: 11px;
}

.irs--roundprobability {
  height: 50px;
  --color: #ffffff;
}
.irs--roundprobability.irs-with-grid {
  height: 65px;
}
.irs--roundprobability .irs-line {
  top: 36px;
  height: 4px;
  background-color: #ffffff;
  background-color: var(--color);
  border-radius: 4px;
}
.irs--roundprobability .irs-bar {
  top: 36px;
  height: 4px;
  background-color: #ffffff;
  background-color: var(--color);
}
.irs--roundprobability .irs-bar2 {
  top: 36px;
  height: 4px;
  background-color: #ffffff;
  background-color: var(--color);
}
.irs--roundprobability .irs-bar3 {
  top: 36px;
  height: 4px;
  background-color: #00ff33;
}
.irs--roundprobability .irs-bar--single {
  border-radius: 4px 0 0 4px;
}
.irs--roundprobability .irs-shadow {
  height: 4px;
  bottom: 21px;
  background-color: rgba(222, 228, 236, 0.5);
}
.irs--roundprobability .irs-handle {
  top: 26px;
  width: 24px;
  height: 24px;
  border: 4px solid #006cfa;
  border-color: var(--color);
  background-color: #ffffff;
  border-radius: 24px;
  box-shadow: 0 1px 3px rgba(0, 0, 255, 0.3);
}
.irs--roundprobability .irs-handle.state_hover,
.irs--roundprobability .irs-handle:hover {
  background-color: #f0f6ff;
}
.irs--roundprobability .irs-min,
.irs--roundprobability .irs-max {
  color: #333;
  font-size: 14px;
  line-height: 1;
  top: 0;
  padding: 3px 5px;
  background-color: rgba(0, 0, 0, 0.1);
  border-radius: 4px;
}
.irs--roundprobability .irs-from,
.irs--roundprobability .irs-to,
.irs--roundprobability .irs-single {
  font-size: 14px;
  line-height: 1;
  text-shadow: none;
  padding: 3px 5px;
  background-color: #006cfa;
  background-color: var(--color);
  color: white;
  border-radius: 4px;
  margin: auto;
}
.irs--roundprobability .irs-from::before,
.irs--roundprobability .irs-to::before,
.irs--roundprobability .irs-single::before {
  position: absolute;
  display: block;
  content: "";
  bottom: -6px;
  left: 50%;
  width: 0;
  height: 0;
  margin-left: -3px;
  overflow: hidden;
  border: 3px solid transparent;
  border-top-color: #006cfa;
}

.irs--roundprobability .irs-from::before {border-top-color: var(--color);}
.irs--roundprobability .irs-to::before {border-top-color: var(--color);}
.irs--roundprobability .irs-single::before {border-top-color: var(--color);}

.irs--roundprobability .irs-grid {
  height: 25px;
}
.irs--roundprobability .irs-grid-pol {
  background-color: #dedede;
}
.irs--roundprobability .irs-grid-text {
  color: silver;
  font-size: 13px;
}

.irs-bar2 {
  display: block;
  background: #006cfa;
  top: 25px;
  position: absolute;
  height: 12px;
}

.irs-bar3 {
  display: block;
  background: #00ff00;
  top: 25px;
  position: absolute;
  height: 12px;
}


code:
///<reference path="model.d.ts"/>
/*
 * Created by aimozg on 29.08.2020.
 */
var Renderer;
(function (Renderer) {
	const millitime = (typeof performance === 'object' && typeof performance.now === 'function') ?
		function () {
			return performance.now();
		} : function () {
		return new Date().getTime();
	};
	Renderer.DefaultImageLoader = {
		loadImage(src, layer, successCallback, errorCallback) {
			const image = new Image();
			image.onload = () => {
				successCallback(src, layer, image);
			};
			image.onerror = (event) => {
				errorCallback(src, layer, event);
			};
			image.src = src;
		}
	};
	Renderer.ImageLoader = Renderer.DefaultImageLoader;
	function rendererError(listener, error, context) {
		if (listener && listener.error) {
			listener.error(error, context);
		}
		else {
			console.error(error);
		}
	}
	/**
	 * Last arguments to composeLayers
	 */
	Renderer.lastCall = undefined;
	/**
	 * Last arguments to animateLayers
	 */
	Renderer.lastAnimateCall = undefined;
	/**
	 * Last result of animateLayers
	 */
	Renderer.lastAnimation = undefined;
	/**
	 * Use "pixels" of this size when generating images.
	 */
	Renderer.pixelSize = 1;
	function emptyLayerFilter() {
		return {
			desaturate: false,
			blend: "",
			blendMode: "",
			brightness: 0.0,
			contrast: 1.0
		};
	}
	Renderer.emptyLayerFilter = emptyLayerFilter;
	/**
	 * 0 -> "#000000", 0.5 -> "#808080", 1.0 -> "#FFFFFF"
	 */
	function gray(value) {
		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;
	}
	Renderer.gray = gray;
	function createCanvas(w, h, fill) {
		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;
	}
	Renderer.createCanvas = createCanvas;
	function ensureCanvas(image) {
		if (image instanceof HTMLCanvasElement) {
			return image;
		}
		let i2 = createCanvas(image.width, image.height);
		i2.drawImage(image, 0, 0);
		return i2.canvas;
	}
	Renderer.ensureCanvas = ensureCanvas;
	/**
	 * Free to use CanvasRenderingContext2D (to create image data, gradients, patterns)
	 */
	Renderer.globalC2D = createCanvas(1, 1);
	/**
	 * Creates a cutout of color in shape of sourceImage
	 */
	function cutout(sourceImage, color, canvas = createCanvas(sourceImage.width, sourceImage.height)) {
		let sw = sourceImage.width;
		let sh = sourceImage.height;
		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);
	}
	Renderer.cutout = cutout;
	/**
	 * Cuts out from base a shape in form of stencil.
	 * Modifies and returns base.
	 */
	function cutoutFrom(base, stencil) {
		base.globalCompositeOperation = 'destination-in';
		base.drawImage(stencil, 0, 0);
		return base;
	}
	Renderer.cutoutFrom = cutoutFrom;
	/**
	 * Paints sourceImage over cutout of it filled with color.
	 */
	function composeOverCutout(sourceImage, color, blendMode = 'multiply', canvas = createCanvas(sourceImage.width, sourceImage.height)) {
		canvas = cutout(sourceImage, color, canvas);
		// Multiply cutout with original
		canvas.globalCompositeOperation = blendMode;
		canvas.drawImage(sourceImage, 0, 0);
		return canvas;
	}
	Renderer.composeOverCutout = composeOverCutout;
	/**
	 * Repeatedly fill all sub-frames of canvas with same style.
	 * (Makes sense with gradient and pattern fills, to keep consistents across all sub-frames)
	 */
	function fillFrames(fillStyle, canvas, frameCount, frameWidth) {
		const frameHeight = canvas.canvas.height;
		canvas.globalCompositeOperation = 'source-over';
		canvas.fillStyle = fillStyle;
		canvas.fillRect(0, 0, frameWidth, frameHeight);
		if (Renderer.pixelSize > 1) {
			// downscale, redraw on temp canvas, then draw again
			const tw = Math.floor(frameWidth / Renderer.pixelSize), th = Math.floor(frameHeight / Renderer.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);
		}
	}
	Renderer.fillFrames = fillFrames;
	Renderer.Patterns = {};
	/**
	 * CanvasPattern generator/provider.
	 * Default implementation looks up in the Renderer.Patterns object, can be replaced to accept complex object
	 * and generate custom pattern.
	 */
	Renderer.PatternProvider = (spec) => {
		if (typeof spec === 'string' && spec in Renderer.Patterns)
			return Renderer.Patterns[spec];
		return null;
	};
	function createGradient(spec) {
		let gradient;
		switch (spec.gradient) {
			case "linear":
				gradient = Renderer.globalC2D.createLinearGradient(spec.values[0], spec.values[1], spec.values[2], spec.values[3]);
				break;
			case "radial":
				gradient = Renderer.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, color;
			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;
	}
	Renderer.createGradient = createGradient;
	/**
	 * Paints sourceImage over same-sized canvas filled with pattern or gradient
	 */
	function composeOverSpecialRect(sourceImage, fillStyle, blendMode, frameCount, targetCanvas = createCanvas(sourceImage.width, sourceImage.height)) {
		let fw = sourceImage.width / frameCount;
		fillFrames(fillStyle, targetCanvas, frameCount, fw);
		targetCanvas.globalCompositeOperation = blendMode;
		targetCanvas.drawImage(sourceImage, 0, 0);
		return targetCanvas;
	}
	Renderer.composeOverSpecialRect = composeOverSpecialRect;
	/**
	 * Paints sourceImage over same-sized canvas filled with color
	 */
	function composeOverRect(sourceImage, color, blendMode, targetCanvas = createCanvas(sourceImage.width, sourceImage.height)) {
		// Fill with target color
		targetCanvas.globalCompositeOperation = 'source-over';
		targetCanvas.fillStyle = color;
		targetCanvas.fillRect(0, 0, sourceImage.width, sourceImage.height);
		targetCanvas.globalCompositeOperation = blendMode;
		targetCanvas.drawImage(sourceImage, 0, 0);
		return targetCanvas;
	}
	Renderer.composeOverRect = composeOverRect;
	/**
	 * Paints over sourceImage a cutout of it filled with color.
	 */
	function composeUnderCutout(sourceImage, color, blendMode = 'multiply', canvas = createCanvas(sourceImage.width, sourceImage.height)) {
		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;
	}
	Renderer.composeUnderCutout = composeUnderCutout;
	/**
	 * Paints over sourceImage a same-sized canvas filled with color
	 */
	function composeUnderRect(sourceImage, color, blendMode = 'multiply', targetCanvas = createCanvas(sourceImage.width, sourceImage.height)) {
		let fill = createCanvas(sourceImage.width, sourceImage.height, color);
		targetCanvas.globalCompositeOperation = 'source-over';
		targetCanvas.drawImage(sourceImage, 0, 0);
		targetCanvas.globalCompositeOperation = blendMode;
		targetCanvas.drawImage(fill.canvas, 0, 0);
		return targetCanvas;
	}
	Renderer.composeUnderRect = composeUnderRect;
	Renderer.ImageCaches = {};
	Renderer.ImageErrors = {};
	/**
	 * Switch between compose(Over|Under)(Rect|Cutout)
	 */
	function compose(composeOver, doCutout, sourceImage, color, blendMode, targetCanvas = createCanvas(sourceImage.width, sourceImage.height)) {
		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);
			}
		}
	}
	Renderer.compose = compose;
	/**
	 * 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
	 */
	function mergeLayerData(target, source, overwrite = false) {
		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;
	}
	Renderer.mergeLayerData = mergeLayerData;
	function encodeProcessing(spec) {
		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
		});
	}
	Renderer.encodeProcessing = encodeProcessing;
	function composeLayersAgain() {
		composeLayers.apply(Renderer, Renderer.lastCall);
	}
	Renderer.composeLayersAgain = composeLayersAgain;
	function desaturateImage(image, resultCanvas, doCutout = true) {
		return compose(false, doCutout, image, '#000000', 'saturation', resultCanvas).canvas;
	}
	Renderer.desaturateImage = desaturateImage;
	function filterImage(image, filter, resultCanvas) {
		if (!resultCanvas) {
			resultCanvas = createCanvas(image.width, image.height);
		}
		resultCanvas.filter = filter;
		resultCanvas.globalCompositeOperation = 'source-over';
		resultCanvas.drawImage(image, 0, 0);
		resultCanvas.filter = '';
		return resultCanvas.canvas;
	}
	Renderer.filterImage = filterImage;
	function adjustBrightness(image, brightness, resultCanvas, doCutout = true) {
		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%
		}
	}
	Renderer.adjustBrightness = adjustBrightness;
	function adjustLevels(image,
	/**
	 * scale factor, 1 - no change, >1 - higher contrast, <1 - lower contrast.
	 */
	factor,
	/**
	 * shift, 0 - no change, >0 - brighter, <0 - darker
	 */
	shift, resultCanvas) {
		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;
		}
	}
	Renderer.adjustLevels = adjustLevels;
	function adjustContrast(image, factor, resultCanvas) {
		/*
		 contrast is scale by F with origin at 0.5
		*/
		const shift = 0.5 * (1 - factor);
		return adjustLevels(image, factor, shift, resultCanvas);
	}
	Renderer.adjustContrast = adjustContrast;
	function adjustBrightnessAndContrast(image, brightness, contrast, resultCanvas) {
		// = adjustContrast (color + brightness, contrast)
		const shift = brightness * contrast + 0.5 * (1 - contrast);
		return adjustLevels(image, contrast, shift, resultCanvas);
	}
	Renderer.adjustBrightnessAndContrast = adjustBrightnessAndContrast;
	const RenderingStepDesaturate = {
		name: "desaturate",
		condition(layer, context) {
			return layer.desaturate;
		},
		render(image, layer, context) {
			context.needsCutout = true;
			return desaturateImage(image, undefined, false);
		}
	};
	const RenderingStepPrefilter = {
		name: "prefilter",
		condition(layer, context) {
			return layer.prefilter && layer.prefilter !== "none";
		},
		render(image, layer, context) {
			return filterImage(image, layer.prefilter);
		}
	};
	const RenderingStepBrightness = {
		name: "brightness",
		condition(layer, context) {
			return typeof layer.brightness === 'number' && layer.brightness !== 0;
		},
		render(image, layer, context) {
			context.needsCutout = true;
			return adjustBrightness(image, layer.brightness, undefined, false);
		}
	};
	const RenderingStepContrast = {
		name: "contrast",
		condition(layer, context) {
			return typeof layer.contrast === 'number' && layer.contrast !== 1;
		},
		render(image, layer, context) {
			context.needsCutout = true;
			return adjustContrast(image, layer.contrast, undefined);
		}
	};
	const RenderingStepBlendColor = {
		name: "blend:color",
		condition(layer, context) {
			return layer.blendMode && layer.blend && typeof layer.blend === "string";
		},
		render(image, layer, context) {
			context.needsCutout = true;
			return composeOverRect(image, layer.blend, layer.blendMode).canvas;
		}
	};
	const RenderingStepBlendGradient = {
		name: "blend:gradient",
		condition(layer, context) {
			return layer.blendMode && layer.blend && typeof layer.blend === 'object' && 'gradient' in layer.blend;
		},
		render(image, layer, context) {
			context.needsCutout = true;
			let gradient = createGradient(layer.blend);
			return composeOverSpecialRect(image, gradient, layer.blendMode, context.rects.subspriteFrameCount).canvas;
		}
	};
	const RenderingStepBlendPattern = {
		name: "blend:pattern",
		condition(layer, context) {
			return layer.blendMode && layer.blend && typeof layer.blend === 'object' && 'pattern' in layer.blend;
		},
		render(image, layer, context) {
			context.needsCutout = true;
			let pattern = Renderer.PatternProvider(layer.blend.pattern);
			if (!pattern) {
				return ensureCanvas(image);
			}
			return composeOverSpecialRect(image, pattern, layer.blendMode, context.rects.subspriteFrameCount).canvas;
		}
	};
	const RenderingStepMask = {
		name: "mask",
		condition(layer, context) {
			return !!layer.mask;
		},
		render(image, layer, context) {
			return cutoutFrom(ensureCanvas(image).getContext('2d'), layer.mask).canvas;
		}
	};
	const RenderingStepCutout = {
		name: "cutout",
		condition(layer, context) {
			return context.needsCutout;
		},
		render(image, layer, context) {
			return cutoutFrom(ensureCanvas(image).getContext('2d'), layer.image).canvas;
		}
	};
	/**
	 * Rendering steps used. Order matters!
	 */
	Renderer.RenderingPipeline = [
		RenderingStepDesaturate,
		RenderingStepPrefilter,
		RenderingStepBrightness,
		RenderingStepContrast,
		RenderingStepBlendPattern,
		RenderingStepBlendGradient,
		RenderingStepBlendColor,
		RenderingStepMask,
		RenderingStepCutout
	];
	function processLayer(layer, rects, listener) {
		let context = {
			layer: layer,
			image: layer.image,
			needsCutout: false,
			rects: rects,
			listener: listener
		};
		for (let step of Renderer.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;
	}
	Renderer.processLayer = processLayer;
	function calcLayerRects(layer, layerImageWidth, targetWidth, targetHeight, frameCount) {
		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
		};
	}
	function composeProcessedLayer(layer, targetCanvas, rects) {
		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);
			}
		}
	}
	Renderer.composeProcessedLayer = composeProcessedLayer;
	function composeLayers(targetCanvas, layerSpecs, frameCount, listener) {
		Renderer.lastCall = [targetCanvas, layerSpecs, frameCount, listener];
		const t0 = millitime();
		// Sort layers by z-index, then array index
		const layers = 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];
		}) // 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]); // 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, 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) {
			Renderer.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;
				Renderer.ImageCaches[src] = image;
				maybeRenderResult();
			}, (src, layer, error) => {
				// Mark this src as erroneous to avoid blinking due to reload attempts
				Renderer.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) {
			Renderer.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;
				Renderer.ImageCaches[src] = image;
				maybeRenderResult();
			}, (src, layer, error) => {
				// Mark this src as erroneous to avoid blinking due to reload attempts
				Renderer.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 (Renderer.ImageErrors[layer.src]) {
					layer.show = false;
					continue;
				}
				else if (layer.src in Renderer.ImageCaches) {
					layer.image = Renderer.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 (Renderer.ImageErrors[layer.masksrc]) {
					delete layer.masksrc;
				}
				else if (layer.masksrc in Renderer.ImageCaches) {
					layer.mask = Renderer.ImageCaches[layer.masksrc];
					layer.cachedMaskSrc = layer.masksrc;
				}
				else {
					loadLayerMask(layer);
				}
			}
		}
		maybeRenderResult();
	}
	Renderer.composeLayers = composeLayers;
	function invalidateLayerCaches(layers) {
		for (let layer of layers) {
			delete layer.image;
			delete layer.imageSrc;
			delete layer.mask;
			delete layer.cachedMaskSrc;
			delete layer.cachedImage;
			delete layer.cachedProcessing;
		}
	}
	Renderer.invalidateLayerCaches = invalidateLayerCaches;
	function animateLayersAgain() {
		return animateLayers.apply(Renderer, Renderer.lastAnimateCall);
	}
	Renderer.animateLayersAgain = animateLayersAgain;
	const animatingCanvases = new WeakMap();
	Renderer.Animations = {};
	/**
	 * 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.
	 */
	Renderer.AnimationProvider = layer => Renderer.Animations[layer.animation];
	/**
	 * Animatable properties of KeyframeSpec and CompositeLayer
	 */
	Renderer.AnimatableProps = ["alpha", "show", "blend", "brightness", "contrast", "dx", "dy"];
	function animateLayers(targetCanvas, layerSpecs, listener, autoStop = true) {
		Renderer.lastAnimateCall = [targetCanvas, layerSpecs, listener, autoStop];
		const keyframeCaches = {};
		function invalidateCaches() {
			for (let key in keyframeCaches)
				delete keyframeCaches[key];
		}
		let schedule = {};
		// this mess should become a class already
		const 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 = {};
				for (let layer of layerSpecs) {
					if (!layer.src || layer.show === false)
						continue;
					if (layer.animation) {
						let spec = Renderer.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 Renderer.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() {
			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) {
			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, layer) {
			layer.frames = [keyframe.frame];
			for (let ap of Renderer.AnimatableProps) {
				if (ap in keyframe)
					layer[ap] = keyframe[ap];
			}
		}
		function nextKeyframe(animation) {
			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() {
			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() {
			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 (Renderer.lastAnimation = animatingCanvas);
	}
	Renderer.animateLayers = animateLayers;
	/**
	 * Linear interpolation.
	 *
	 * f(0) = min,
	 * f(1) = max.
	 */
	function lint(value, min, max, allowOverflow = false) {
		if (!allowOverflow)
			value = Math.min(1, Math.max(0, value));
		return value * (max - min) + min;
	}
	Renderer.lint = lint;
	function lintArray(value, mins, maxes, allowOverflow = false) {
		return mins.map((min, i) => lint(value, min, maxes[i], allowOverflow));
	}
	Renderer.lintArray = lintArray;
	function lintStaged(value, points) {
		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]);
	}
	Renderer.lintStaged = lintStaged;
	function lintRgb(value, min, max) {
		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)
		});
	}
	Renderer.lintRgb = lintRgb;
	function lintRgbStaged(value, points) {
		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]);
	}
	Renderer.lintRgbStaged = lintRgbStaged;
	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;
})(Renderer || (Renderer = {}));