blob: 67f0b1649b828f882ac1cb03d462a5ef3f441ca4 [file]
/**
* @module skottie-color-manager-sk
* @description <h2><code>skottie-color-manager-sk</code></h2>
*
* <p>
* A component to enable changing colors of a lottie animation.
* It works by wrapping the animation in a precomp and applying
* a color filter to the outer composition.
* It uses a json template that works as a wrapper that will contain
* the original animation.
* </p>
*
* @evt animation-updated - This event is triggered every time the manager is updated,
* either from applying the filter or when its properties change.
*
*/
import { html, TemplateResult } from 'lit-html';
import { define } from '../../../elements-sk/modules/define';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { LottieAnimation, LottieCompAsset, LottieTintEffect } from '../types';
import { colorToHex, hexToColor } from '../helpers/color';
import '../skottie-color-input-sk';
import { SkottieColorEventDetail } from '../skottie-color-input-sk/skottie-color-input-sk';
const TEMPLATE_ID = 'template_0';
const TINT_EFFECT_TYPE = 20;
// Tint effect template
const tintEffectTemplate: LottieTintEffect = {
ty: TINT_EFFECT_TYPE, // Tint effect
nm: 'Tint',
np: 6,
mn: 'ADBE Tint',
en: 1,
ef: [
{
ty: 2,
nm: 'Map Black To',
mn: 'ADBE Tint-0001',
v: {
a: 0,
k: [0, 0, 0, 0],
},
},
{
ty: 2,
nm: 'Map White To',
mn: 'ADBE Tint-0002',
v: {
a: 0,
k: [1, 1, 1, 0],
},
},
{
ty: 0,
nm: 'Amount to Tint',
mn: 'ADBE Tint-0003',
v: {
a: 0,
k: 100,
},
},
],
};
// Animation template
const animationTemplate: LottieAnimation = {
w: 0, // this should be changed to match the animation width
h: 0, // this should be changed to match the animation height
ip: 0, // this should be changed to match the animation in point
op: 0, // this should be changed to match the animation out point
fr: 0, // this should be changed to match the animation frame rate
v: '5.0.0', // this should be changed to match the animation version
ddd: 0,
assets: [
{
id: TEMPLATE_ID,
nm: 'template_precomp',
fr: 0,
layers: [], // Here we'll insert the animation layers
},
],
layers: [
{
ty: 0, // precomp
ind: 1,
nm: 'template',
ddd: 0,
refId: TEMPLATE_ID,
ef: [tintEffectTemplate],
sr: 1, // no stretch
bm: 0, // normal blend mode
ks: {
// Default values for transform properties
o: {
a: 0,
k: 100,
},
r: {
a: 0,
k: 0,
},
p: {
a: 0,
k: [0, 0, 0],
l: 2,
},
a: {
a: 0,
k: [0, 0, 0],
l: 2,
},
s: {
a: 0,
k: [100, 100, 100],
l: 2,
},
},
ao: 0, // no auto orient
w: 0, // this should be changed to match the animation width
h: 0, // this should be changed to match the animation height
ip: 0, // this should be changed to match the animation in point
op: 0, // this should be changed to match the animation out point
st: 0, // this should be changed to match the animation start point
},
],
};
export interface SkottieTemplateEventDetail {
animation: LottieAnimation;
}
export class SkottieColorManagerSk extends ElementSk {
private _animation: LottieAnimation | null = null;
private static template = (ele: SkottieColorManagerSk) => html`
<div class="wrapper">${ele.renderView()}</div>
`;
constructor() {
super(SkottieColorManagerSk.template);
}
connectedCallback(): void {
super.connectedCallback();
this._render();
}
disconnectedCallback(): void {
super.disconnectedCallback();
}
private hasFontManager(): boolean {
// We are using some conditions to check if this animation
// has already been wrapped with a font manager.
// There could be some false positives but it's unlikely.
// 1. it has a single layer in the layers list
// 2. the layer is of type precomp
// 3. the layer has a single effect applied to it
// 4. the effect is a tint effect
if (this._animation) {
if (
this._animation.layers.length === 1 &&
this._animation.layers[0].refId === TEMPLATE_ID &&
this._animation.layers[0].ef &&
this._animation.layers[0].ef.length &&
this._animation.layers[0].ef[0].ty === TINT_EFFECT_TYPE
) {
return true;
}
}
return false;
}
private renderManagedAnimation(): TemplateResult {
const effect = this._animation?.layers?.[0].ef?.[0] as LottieTintEffect;
const blackColor = colorToHex(effect.ef[0].v.k);
const whiteColor = colorToHex(effect.ef[1].v.k);
const blendValue = effect.ef[2].v.k;
return html`
<div class="manager">
<div class="color-form--color">
<span>Map Black to</span>
<skottie-color-input-sk
.color=${blackColor}
@color-change=${this.onMapBlackChange}></skottie-color-input-sk>
</div>
<div class="color-form--color">
<span>Map White to</span>
<skottie-color-input-sk
.color=${whiteColor}
@color-change=${this.onMapWhiteChange}></skottie-color-input-sk>
</div>
<label class="color-form--original-color">
<input
type="checkbox"
@change=${this.onToggleOriginal}
?checked=${blendValue === 0} />
<span class="box"></span>
<span class="label">Show original color scheme</span>
</label>
</div>
`;
}
private renderUnmanagedAnimation(): TemplateResult {
return html`
<div class="no-manager">
<div class="info-box">
<span class="icon-sk info-box--icon">info</span>
<span class="info-box--description">
The color manager will modify the Json file to allow color editing
and it will not be possible to revert it
</span>
</div>
<skottie-button-sk
id="view-perf-chart"
@select=${this.applyColorManager}
.content=${'Apply color manager'}
.classes=${['apply-btn']}>
</skottie-button-sk>
</div>
`;
}
private applyColorManager(): void {
if (!this._animation) {
return;
}
// Making a copy of the template to avoid writing on the original one
const template: LottieAnimation = JSON.parse(
JSON.stringify(animationTemplate)
);
const animation = this._animation;
template.v = animation.v;
template.w = animation.w;
template.h = animation.h;
template.ip = animation.ip;
template.op = animation.op;
template.fr = animation.fr;
template.fonts = animation.fonts;
const assets = template.assets;
const compAsset = assets[0] as LottieCompAsset;
compAsset.layers = this._animation.layers;
this._animation.assets.forEach((asset) => {
assets.push(asset);
});
template.props = this._animation.props;
template.markers = this._animation.markers;
const rootLayer = template.layers[0];
rootLayer.w = animation.w;
rootLayer.h = animation.h;
rootLayer.ip = animation.ip;
rootLayer.op = animation.op;
JSON.stringify(template);
this.animation = template;
this.dispatch();
}
private renderNoAnomation(): TemplateResult {
return html`<span>No active animation</span>`;
}
private renderView(): TemplateResult {
if (!this._animation) {
return this.renderNoAnomation();
}
if (this.hasFontManager()) {
return this.renderManagedAnimation();
}
return this.renderUnmanagedAnimation();
}
private onMapBlackChange(ev: CustomEvent<SkottieColorEventDetail>): void {
if (!this._animation) {
return;
}
const color = hexToColor(ev.detail.color);
const effect = this._animation.layers?.[0].ef?.[0] as LottieTintEffect;
// Index 0 of effect is black map
effect.ef[0].v.k = color;
this.dispatch();
this._render();
}
private onMapWhiteChange(ev: CustomEvent<SkottieColorEventDetail>): void {
if (!this._animation) {
return;
}
const color = hexToColor(ev.detail.color);
const effect = this._animation.layers?.[0].ef?.[0] as LottieTintEffect;
// Index 1 of effect is white map
effect.ef[1].v.k = color;
this.dispatch();
this._render();
}
private onToggleOriginal(ev: Event): void {
if (!this._animation) {
return;
}
const checkboxElement = ev.target as HTMLInputElement;
const merge = checkboxElement.checked === true ? 0 : 100;
const effect = this._animation.layers?.[0].ef?.[0] as LottieTintEffect;
// Index 2 of effect is blending with original value
// 100 is fully blend, 0 is no blend
effect.ef[2].v.k = merge;
this.dispatch();
this._render();
}
dispatch(): void {
if (this._animation) {
this.dispatchEvent(
new CustomEvent<SkottieTemplateEventDetail>('animation-updated', {
detail: {
animation: this._animation,
},
bubbles: true,
})
);
}
}
set animation(value: LottieAnimation) {
this._animation = value;
this._render();
}
}
define('skottie-color-manager-sk', SkottieColorManagerSk);