blob: a13e5f11e59bc02c0ef6308babf874dad76f8015 [file] [log] [blame]
/**
* @module skottie-config-sk
* @description <h2><code>skottie-config-sk</code></h2>
*
* <p>
* A dialog for configuring how to render a lottie file.
* </p>
*
* <p>
* The form of the 'state' property looks like a serialized UploadRequest:
* </p>
* <pre>
* {
* filename: 'foo.json',
* lottie: {},
* assetsZip: 'data:application/zip;base64,...'
* assetsFileName: 'assets.zip'
* }
* <pre>
*
* @evt skottie-selected - This event is generated when the user presses Go.
* The updated state, width, and height is available in the event detail.
* There is also an indication if the lottie file was changed.
*
* @evt cancelled - This event is generated when the user presses Cancel.
*
*/
import 'elements-sk/styles/buttons';
import { define } from 'elements-sk/define';
import { errorMessage } from 'elements-sk/errorMessage';
import { html } from 'lit-html';
import { $$ } from 'common-sk/modules/dom';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { SoundMap } from '../audio';
import { LottieAnimation } from '../types';
const DEFAULT_SIZE = 128;
const BACKGROUND_VALUES = {
TRANSPARENT: 'rgba(0,0,0,0)',
LIGHT: '#FFFFFF',
DARK: '#000000',
};
const allowZips = window.location.hostname === 'skottie-internal.skia.org'
|| window.location.hostname === 'localhost';
export interface SkottieConfigState {
assets?: Record<string, ArrayBuffer>;
filename: string,
lottie: LottieAnimation | null,
assetsZip: string,
assetsFilename: string,
w?: number,
h?: number,
soundMap?: SoundMap,
}
export interface SkottieConfigEventDetail {
state: SkottieConfigState,
fileChanged: boolean,
width: number,
height: number,
fps: number,
backgroundColor: string,
}
export class SkottieConfigSk extends ElementSk {
private static template = (ele: SkottieConfigSk) => html`
<div ?hidden=${!allowZips}>
We support 3 types of uploads:
<ul>
<li>A plain JSON file.</li>
<li>A JSON file with a zip file of assets (e.g. images) used by the animation.</li>
<li>
A zip file produced by lottiefiles.com
(<a href="https://lottiefiles.com/1187-puppy-run">example</a>)
with a JSON file in the top level and an images/ directory.
</li>
</ul>
</div>
<label class=button-like>Lottie file to upload
<input type=file name=file id=file @change=${ele.onFileChange}/>
</label>
<div class="filename input-like ${ele._state.filename ? '' : 'empty'}">
${ele._state.filename ? ele._state.filename : 'No file selected.'}
</div>
<label class=button-like ?hidden=${!allowZips}>Optional Asset Folder (.zip)
<input type=file name=folder id=folder @change=${ele.onFolderChange}/>
</label>
<div class="filename input-like ${ele._state.assetsFilename ? '' : 'empty'}" ?hidden=${!allowZips}>
${ele._state.assetsFilename ? ele._state.assetsFilename : 'No asset folder selected.'}
</div>
<label class=number>
Background Color
<select id="backgroundColor">
<option
value=${BACKGROUND_VALUES.TRANSPARENT}
?selected=${ele._backgroundColor === BACKGROUND_VALUES.TRANSPARENT}
>Transparent</option>
<option
value=${BACKGROUND_VALUES.LIGHT}
?selected=${ele._backgroundColor === BACKGROUND_VALUES.LIGHT}
>Light</option>
<option
value=${BACKGROUND_VALUES.DARK}
?selected=${ele._backgroundColor === BACKGROUND_VALUES.DARK}
>Dark</option>
</select>
</label>
<checkbox-sk label="Lock aspect ratio"
?checked=${ele._isRatioLocked}
@click=${ele.toggleRatioLock}>
</checkbox-sk>
<label class=number>
<input type=number id=width @change=${ele.onWidthInput}
.value=${ele._width} required /> Width (px)
</label>
<label class=number>
<input type=number id=height @change=${ele.onHeightInput}
.value=${ele._height} required /> Height (px)
</label>
<label class=number>
<input type=number id=fps .value=${ele._fps} required /> FPS
</label>
<div>
0 for width/height means use the default from the animation. For FPS, 0 means "as smooth as possible"
and -1 means "use what the animation says".
</div>
<div class=warning ?hidden=${ele.warningHidden()}>
<p>
The width or height of your file exceeds 1024, which may not fit on the screen.
Press a 'Rescale' button to fix the dimensions while preserving the aspect ratio.
</p>
<div>
<button @click=${() => ele.rescale(1024)}>Rescale to 1024</button>
<button @click=${() => ele.rescale(512)}>Rescale to 512</button>
<button @click=${() => ele.rescale(128)}>Rescale to 128</button>
</div>
</div>
<div id=dialog-buttons>
${ele.cancelButton()}
<button class=action ?disabled=${ele.readyToGo()} @click=${ele.go}>Go</button>
</div>
`;
private cancelButton = () => {
if (this.hasCancel()) {
return html`<button id=cancel @click=${this.cancel}>Cancel</button>`;
}
return html``;
};
private _state: SkottieConfigState = {
filename: '',
lottie: null,
assetsZip: '',
assetsFilename: '',
};
private _isRatioLocked: boolean = false;
private _ratio: number = 0;
private _width: number = DEFAULT_SIZE;
private _height: number = DEFAULT_SIZE;
private _fps: number = 0;
private _backgroundColor: string = BACKGROUND_VALUES.TRANSPARENT;
private _fileChanged: boolean = false;
constructor() {
super(SkottieConfigSk.template);
}
connectedCallback(): void {
super.connectedCallback();
this._render();
this.addEventListener('input', this.inputEvent);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener('input', this.inputEvent);
}
get height(): number { return this._height; }
set height(val: number) {
this._height = val;
this._render();
}
get state(): SkottieConfigState { return this._state; }
set state(val: SkottieConfigState) {
this._state = Object.assign({}, val); // make a copy of passed in state.
this._render();
}
get fps(): number { return this._fps; }
set fps(val: number) {
this._fps = +val;
this._render();
}
get width(): number { return this._width; }
set width(val: number) {
this._width = +val;
this._render();
}
get backgroundColor(): string { return this._backgroundColor; }
set backgroundColor(val: string) {
this._backgroundColor = val;
this._render();
}
private hasCancel(): boolean {
return !!this._state.lottie;
}
private readyToGo(): boolean {
return !this._state.filename && (!!this._state.lottie || !!this._state.assetsZip);
}
private onFileChange(e: Event): void {
const files = (e.target as HTMLInputElement).files!;
this._fileChanged = true;
const toLoad = files[0];
const reader = new FileReader();
if (toLoad.name.endsWith('.json')) {
reader.addEventListener('load', () => {
let parsed: LottieAnimation;
try {
parsed = JSON.parse(reader.result as string) as LottieAnimation;
} catch (error) {
errorMessage(`Not a valid JSON file: ${error}`);
return;
}
this._state.lottie = parsed;
this._state.filename = toLoad.name;
this._width = parsed.w || DEFAULT_SIZE;
this._height = parsed.h || DEFAULT_SIZE;
this._render();
});
reader.addEventListener('error', () => {
errorMessage('Failed to load.');
});
reader.readAsText(toLoad);
} else if (allowZips && toLoad.name.endsWith('.zip')) {
reader.addEventListener('load', () => {
this._state.lottie = null;
this._state.assetsZip = reader.result as string;
this._state.filename = toLoad.name;
this._width = DEFAULT_SIZE;
this._height = DEFAULT_SIZE;
this._render();
});
reader.addEventListener('error', () => {
errorMessage(`Failed to load ${toLoad.name}`);
});
reader.readAsDataURL(toLoad);
} else {
let msg = `Bad file type ${toLoad.name}, only .json and .zip supported`;
if (!allowZips) {
msg = `Bad file type ${toLoad.name}, only .json supported`;
}
errorMessage(msg);
this._state.filename = '';
this._state.lottie = null;
}
}
private onFolderChange(e: Event): void {
const files = (e.target as HTMLInputElement).files!;
this._fileChanged = true;
const toLoad = files[0];
const reader = new FileReader();
reader.addEventListener('load', () => {
this._state.assetsZip = reader.result as string;
this._state.assetsFilename = toLoad.name;
this._render();
});
reader.addEventListener('error', () => {
errorMessage(`Failed to load ${toLoad.name}`);
});
reader.readAsDataURL(toLoad);
}
private toggleRatioLock(e: Event): void {
e.preventDefault();
this._isRatioLocked = !this._isRatioLocked;
this._ratio = this._isRatioLocked ? (this._width / this._height) : 0;
this._render();
}
private onWidthInput(): void {
if (this._isRatioLocked) {
this._height = Math.floor(this._width / this._ratio);
this._render();
}
}
private onHeightInput(): void {
if (this._isRatioLocked) {
this._width = Math.floor(this._height * this._ratio);
this._render();
}
}
private rescale(n: number): void {
const max = Math.max(this._width, this._height);
if (max <= n) {
return;
}
this._width = Math.floor((this._width * n) / max);
this._height = Math.floor((this._height * n) / max);
this._render();
}
private warningHidden(): boolean {
return this._width <= 1024 && this._width <= 1024;
}
private updateState(): void {
this._width = +$$<HTMLInputElement>('#width', this)!.value;
this._height = +$$<HTMLInputElement>('#height', this)!.value;
this._fps = +$$<HTMLInputElement>('#fps', this)!.value;
this._backgroundColor = $$<HTMLInputElement>('#backgroundColor', this)!.value;
}
private go(): void {
this.updateState();
this.dispatchEvent(new CustomEvent<SkottieConfigEventDetail>('skottie-selected', {
detail: {
state: this._state,
fileChanged: this._fileChanged,
width: this._width,
height: this._height,
fps: this._fps,
backgroundColor: this._backgroundColor,
},
bubbles: true,
}));
}
private cancel(): void {
this.dispatchEvent(new CustomEvent('cancelled', { bubbles: true }));
}
private inputEvent(): void {
this.updateState();
this._render();
}
}
define('skottie-config-sk', SkottieConfigSk);