blob: 78d94b30aa66f8b5a745a03070d60a879f1d3bdc [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 { html } from 'lit-html';
import { define } from '../../../elements-sk/modules/define';
import { errorMessage } from '../../../elements-sk/modules/errorMessage';
import { $$ } from '../../../infra-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);