├── example
├── App.css
├── main.jsx
├── index.css
└── App.tsx
├── .gitignore
├── vite.config.js
├── .prettierrc.yml
├── .prettierrc.js
├── index.html
├── src
├── index.ts
├── property
│ ├── ValueProperty.ts
│ ├── MultiDimensionalProperty.ts
│ ├── KeyframedValueProperty.ts
│ ├── BaseProperty.ts
│ └── KeyframedMultidimensionalProperty.ts
├── element
│ ├── CompLottieElement.ts
│ ├── SpriteLottieElement.ts
│ ├── TextLottieElement.ts
│ └── BaseLottieElement.ts
├── tools.ts
├── EditorLottieLoader.ts
├── bezier.ts
├── LottieLoader.ts
├── Expression.ts
├── TransformFrames.ts
├── LottieResource.ts
└── LottieAnimation.ts
├── LEGAL.md
├── tsconfig.json
├── .eslintrc.js
├── LICENSE
├── rollup.miniprogram.plugin.js
├── .github
└── workflows
│ └── release.yml
├── README.md
├── package.json
└── rollup.config.js
/example/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | #canvas {
6 | display: block;
7 | width: 100vw;
8 | height: 100vh;
9 | overflow: hidden;
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | coverage
5 | types
6 | tsconfig.tsbuildinfo
7 | .aci.yml
8 | package-lock.json
9 | yarn.lock
10 | .npmrc
11 |
12 | pnpm-lock.yaml
13 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | const config = {
5 | jsx: "react",
6 | optimizeDeps: {
7 | exclude: ["@galacean/engine"]
8 | }
9 | };
10 |
11 | export default config;
12 |
--------------------------------------------------------------------------------
/.prettierrc.yml:
--------------------------------------------------------------------------------
1 | # More detail:https://prettier.io/docs/en/configuration.html
2 |
3 | ---
4 | printWidth: 120
5 | proseWrap: never
6 | singleQuote: false
7 | trailingComma: none
8 | semi: true
9 | tabWidth: 2
10 | useTabs: false
11 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | semi: true,
4 | singleQuote: false,
5 | trailingComma: 'all',
6 | bracketSpacing: true,
7 | jsxBracketSameLine: true,
8 | insertPragma: false,
9 | tabWidth: 2,
10 | useTabs: true,
11 | }
12 |
--------------------------------------------------------------------------------
/example/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 | import App from './App'
5 |
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | )
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Loader } from "@galacean/engine";
2 | import { LottieAnimation } from "./LottieAnimation";
3 |
4 | export { EditorLottieLoader } from "./EditorLottieLoader";
5 | export { LottieLoader } from "./LottieLoader";
6 | export { LottieResource } from "./LottieResource";
7 | export { LottieAnimation };
8 |
9 | Loader.registerClass("LottieAnimation", LottieAnimation);
10 |
--------------------------------------------------------------------------------
/LEGAL.md:
--------------------------------------------------------------------------------
1 | Legal Disclaimer
2 |
3 | Within this source code, the comments in Chinese shall be the original, governing version. Any comment in other languages are for reference only. In the event of any conflict between the Chinese language version comments and other language version comments, the Chinese language version shall prevail.
4 |
5 | 法律免责声明
6 |
7 | 关于代码注释部分,中文注释为官方版本,其它语言注释仅做参考。中文注释可能与其它语言注释存在不一致,当中文注释与其它语言注释存在不一致时,请以中文注释为准。
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "target": "esnext",
5 | "declaration": true,
6 | "moduleResolution": "node",
7 | "allowSyntheticDefaultImports": true,
8 | "experimentalDecorators": true,
9 | "sourceMap": true,
10 | "incremental": true,
11 | "skipLibCheck": true,
12 | "declarationDir": "types",
13 | "resolveJsonModule": true
14 | },
15 | "include": ["src/**/*"]
16 | }
17 |
--------------------------------------------------------------------------------
/example/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser", //定义ESLint的解析器
3 | extends: ["alloy", "alloy/typescript", "prettier/@typescript-eslint", "plugin:prettier/recommended"], //定义文件继承的子规范
4 | plugins: ["@typescript-eslint"], //定义了该eslint文件所依赖的插件
5 | parserOptions: {
6 | ecmaVersion: 2019,
7 | sourceType: "module",
8 | ecmaFeatures: {
9 | jsx: true,
10 | },
11 | },
12 | env: {
13 | //指定代码的运行环境
14 | browser: true,
15 | node: true,
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/property/ValueProperty.ts:
--------------------------------------------------------------------------------
1 | import BaseProperty from "./BaseProperty";
2 | import { TypeValueProperty } from "./BaseProperty";
3 |
4 | /**
5 | * unidimensional value property
6 | * @internal
7 | */
8 | export default class ValueProperty extends BaseProperty {
9 | constructor(data: TypeValueProperty, mult: number = 1) {
10 | super(data, mult);
11 | this.v = mult ? this.value * mult : this.value;
12 | }
13 |
14 | update() {
15 | this.v = this.value * this.mult;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/property/MultiDimensionalProperty.ts:
--------------------------------------------------------------------------------
1 | import BaseProperty, { TypeMultiDimensionalProperty } from "./BaseProperty";
2 |
3 | /**
4 | * multidimensional value property
5 | */
6 | export default class MultiDimensionalProperty extends BaseProperty {
7 | constructor(data: TypeMultiDimensionalProperty, mult = 1) {
8 | super(data, mult);
9 | const len = this.value.length;
10 | this.v = new Float32Array(len);
11 | this.newValue = new Float32Array(len);
12 |
13 | for (let i = 0; i < len; i += 1) {
14 | this.v[i] = this.value[i] * this.mult;
15 | }
16 | }
17 |
18 | update() {
19 | let finalValue: number[];
20 |
21 | finalValue = this.value;
22 |
23 | for (let i = 0, len = this.v.length; i < len; i++) {
24 | this.v[i] = finalValue[i] * this.mult;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/element/CompLottieElement.ts:
--------------------------------------------------------------------------------
1 | import { Engine, Entity } from "@galacean/engine";
2 | import BaseLottieElement from "./BaseLottieElement";
3 |
4 | /**
5 | * @internal
6 | */
7 | export default class CompLottieElement extends BaseLottieElement {
8 | layers: any;
9 | comps: [];
10 |
11 | constructor(layer, engine?: Engine, entity?: Entity, name?: string) {
12 | super(layer);
13 |
14 | this.layers = layer.layers;
15 | this.comps = layer.comps;
16 |
17 | if (entity) {
18 | this.entity = entity;
19 | if (name) {
20 | this.entity.name = name;
21 | }
22 | } else {
23 | const compEntity = new Entity(engine, name);
24 | this.entity = compEntity;
25 | }
26 | }
27 |
28 | destroy() {
29 | super.destroy();
30 | this.layers = null;
31 | this.comps = null;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/tools.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * some useful toolkit
3 | * @namespace
4 | */
5 | const Tools = {
6 | /**
7 | * euclidean modulo
8 | * @method
9 | * @param {Number} n input value
10 | * @param {Number} m modulo
11 | * @return {Number} re-map to modulo area
12 | */
13 | euclideanModulo: function (n, m) {
14 | return ((n % m) + m) % m;
15 | },
16 |
17 | /**
18 | * bounce value when value spill codomain
19 | * @method
20 | * @param {Number} n input value
21 | * @param {Number} min lower boundary
22 | * @param {Number} max upper boundary
23 | * @return {Number} bounce back to boundary area
24 | */
25 | codomainBounce: function (n, min, max) {
26 | if (n < min) return 2 * min - n;
27 | if (n > max) return 2 * max - n;
28 | return n;
29 | },
30 |
31 | /**
32 | * clamp a value in range
33 | * @method
34 | * @param {Number} x input value
35 | * @param {Number} a lower boundary
36 | * @param {Number} b upper boundary
37 | * @return {Number} clamp in range
38 | */
39 | clamp: function (x, a, b) {
40 | return x < a ? a : x > b ? b : x;
41 | }
42 | };
43 |
44 | export default Tools;
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) bob (Ant Financial)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/EditorLottieLoader.ts:
--------------------------------------------------------------------------------
1 | import { AssetPromise, AssetType, LoadItem, Loader, ResourceManager, Utils, resourceLoader } from "@galacean/engine";
2 | import { LottieResource, TypeRes } from "./LottieResource";
3 |
4 | /**
5 | * @internal
6 | */
7 | // @ts-ignore
8 | @resourceLoader("EditorLottie", ["json"])
9 | export class EditorLottieLoader extends Loader {
10 | // @ts-ignore
11 | load(item: LoadItem, resourceManager: ResourceManager): AssetPromise {
12 | return new AssetPromise((resolve) => {
13 | const { url } = item;
14 | // @ts-ignore
15 | resourceManager._request(url, { type: "json" }).then((data) => {
16 | const { jsonUrl, atlasUrl } = data;
17 | // @ts-ignore
18 | const jsonPromise = resourceManager._request(Utils.resolveAbsoluteUrl(url, jsonUrl), resourceManager);
19 | const atlasPromise = resourceManager.load({
20 | url: Utils.resolveAbsoluteUrl(url, atlasUrl),
21 | type: AssetType.SpriteAtlas
22 | });
23 |
24 | AssetPromise.all([jsonPromise, atlasPromise]).then(([res, atlas]) => {
25 | const { engine } = resourceManager;
26 | const resource = new LottieResource(engine, res as TypeRes, atlas);
27 | resolve(resource);
28 | });
29 | });
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/property/KeyframedValueProperty.ts:
--------------------------------------------------------------------------------
1 | import Expression from "../Expression";
2 | import BaseProperty, { TypeKeyframe, TypeValueKeyframedProperty } from "./BaseProperty";
3 |
4 | /**
5 | * keyframed unidimensional value property
6 | */
7 | export default class KeyframedValueProperty extends BaseProperty {
8 | private _value: number = 0;
9 |
10 | constructor(data: TypeValueKeyframedProperty, mult: number = 1) {
11 | super(data, mult);
12 | this.v = 0;
13 |
14 | if (Expression.hasSupportExpression(data)) {
15 | this.expression = Expression.getExpression(data);
16 | }
17 | }
18 |
19 | reset() {
20 | this._value = 0;
21 | }
22 |
23 | update(frameNum: number) {
24 | if (this.expression) {
25 | frameNum = this.expression.update(frameNum);
26 | }
27 |
28 | const { value } = this;
29 |
30 | let keyData: TypeKeyframe;
31 | let nextKeyData: TypeKeyframe;
32 |
33 | // Find current frame
34 | for (let i = 0, l = value.length - 1; i < l; i++) {
35 | keyData = value[i];
36 | nextKeyData = value[i + 1];
37 | if (nextKeyData.t > frameNum) {
38 | break;
39 | }
40 | }
41 |
42 | if (!keyData.beziers) {
43 | keyData.beziers = [];
44 | }
45 |
46 | this.v = this.getValue(frameNum, 0, keyData, nextKeyData) * this.mult;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/rollup.miniprogram.plugin.js:
--------------------------------------------------------------------------------
1 | import inject from "@rollup/plugin-inject";
2 | import modify from "rollup-plugin-modify";
3 | const path = require("path");
4 |
5 | const module = "@galacean/engine-miniprogram-adapter";
6 |
7 | function register(name) {
8 | return [module, name];
9 | }
10 |
11 | const adapterArray = [
12 | "btoa",
13 | "URL",
14 | "Blob",
15 | "window",
16 | "atob",
17 | "devicePixelRatio",
18 | "document",
19 | "Element",
20 | "Event",
21 | "EventTarget",
22 | "HTMLCanvasElement",
23 | "HTMLElement",
24 | "HTMLMediaElement",
25 | "HTMLVideoElement",
26 | "Image",
27 | "navigator",
28 | "Node",
29 | "requestAnimationFrame",
30 | "cancelAnimationFrame",
31 | "screen",
32 | "XMLHttpRequest",
33 | "performance",
34 | "WebGLRenderingContext",
35 | "WebGL2RenderingContext",
36 | "ImageData",
37 | "location",
38 | "OffscreenCanvas",
39 | ];
40 | const adapterVars = {};
41 |
42 | adapterArray.forEach((name) => {
43 | adapterVars[name] = register(name);
44 | });
45 |
46 | const regStr = [`"@galacean/engine"`, `'@galacean/engine'`].join("|");
47 |
48 | export default [
49 | inject(adapterVars),
50 | modify({
51 | find: new RegExp(regStr, "g"),
52 | replace: (match, moduleName) => {
53 | return `${match.substr(0, match.length - 1)}/dist/miniprogram"`;
54 | },
55 | }),
56 | ];
57 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Release
4 |
5 | on:
6 | push:
7 | tags:
8 | - "v*"
9 | workflow_dispatch:
10 | inputs:
11 | specific_tag:
12 | description: 'Specific tag to release'
13 | required: false
14 | default: ''
15 | jobs:
16 | release:
17 | runs-on: ubuntu-latest
18 | permissions:
19 | contents: write
20 | id-token: write
21 | steps:
22 | - uses: actions/checkout@v4
23 | with:
24 | fetch-depth: 0
25 |
26 | - name: Install pnpm
27 | uses: pnpm/action-setup@v4
28 | with:
29 | run_install: true
30 |
31 | # after pnpm
32 | - name: Use Node.js LTS
33 | uses: actions/setup-node@v4
34 | with:
35 | node-version: '>=22.6.0'
36 | registry-url: https://registry.npmjs.org/
37 | cache: pnpm
38 |
39 | - name: Build
40 | run: pnpm build
41 |
42 | - name: Release current monorepo
43 | uses: galacean/publish@main
44 | with:
45 | specific_tag: ${{ inputs.specific_tag }}
46 | publish: false
47 | packages: |
48 | ./
49 | env:
50 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
51 | NPM_CONFIG_PROVENANCE: true
52 | OASISBE_UPLOAD_URL: https://oasisbe.alipay.com/api/file/no-auth/crypto/upload
53 | OASISBE_REQUEST_HEADER: ${{secrets.OASISBE_REQUEST_HEADER}}
54 | OASISBE_PUBLIC_KEY: ${{secrets.OASISBE_PUBLIC_KEY}}
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Galacean Lottie
2 |
3 |
4 | 
5 | 
6 |
7 | This is a [lottie](https://airbnb.io/lottie) runtime created by [Galacean Engine](https://github.com/galacean/engine). It is high-performance owing to drawing all sprites in batch, which is different from svg or canvas renderer of [lottie-web](https://github.com/airbnb/lottie-web).
8 |
9 | See more info: [documentation](https://galacean.antgroup.com/engine/docs/graphics/2D/lottie/)
10 |
11 | ## Features
12 | - [x] Sprite element: transform and opacity animation.
13 | - [x] Text element
14 | - [x] 3D rotation: rotate element in 3D space.
15 | - [x] Animation clip: play animation clip.
16 |
17 | #### TODO
18 | - [ ] Sprite mask
19 | - [ ] Shape element
20 | - [ ] Expression
21 |
22 | ## Usage
23 |
24 | See: https://galacean.antgroup.com/engine/docs/graphics/2D/lottie/
25 |
26 | ## Install
27 |
28 | ```bash
29 | npm i @galacean/engine-lottie --save
30 | ```
31 |
32 | ## Contributing
33 | Everyone is welcome to create issues or submit pull requests. Make sure to read the [Contributing Guide](https://github.com/galacean/engine/blob/main/.github/HOW_TO_CONTRIBUTE.md) before submitting changes.
34 |
35 | ## Dev
36 |
37 | 1.Clone this repository and install the dependencies:
38 |
39 | ```bash
40 | npm i
41 | ```
42 |
43 | 2.Run the example:
44 |
45 | ```bash
46 | npm run example
47 | ```
48 | ## License
49 |
50 | MIT
51 |
--------------------------------------------------------------------------------
/src/element/SpriteLottieElement.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Sprite, SpriteAtlas, SpriteRenderer, TextureWrapMode } from "@galacean/engine";
2 | import BaseLottieElement from "./BaseLottieElement";
3 |
4 | /**
5 | * @internal
6 | */
7 | export default class SpriteLottieElement extends BaseLottieElement {
8 | sprite: Sprite;
9 | spriteRenderer: SpriteRenderer;
10 |
11 | constructor(layer, atlas: SpriteAtlas, entity: Entity, childEntity?: Entity) {
12 | super(layer);
13 |
14 | let spriteRenderer;
15 |
16 | if (layer.refId) {
17 | if (childEntity) {
18 | this.entity = childEntity;
19 | spriteRenderer = childEntity.getComponent(SpriteRenderer);
20 | this.sprite = spriteRenderer.sprite;
21 | } else {
22 | this.sprite = atlas.getSprite(layer.refId);
23 | const spriteEntity = new Entity(entity.engine, layer.nm);
24 | spriteRenderer = spriteEntity.addComponent(SpriteRenderer);
25 | spriteRenderer.sprite = this.sprite;
26 | this.entity = spriteEntity;
27 | }
28 |
29 | const { atlasRegion, texture } = this.sprite;
30 | texture.wrapModeU = texture.wrapModeV = TextureWrapMode.Clamp;
31 | this.spriteRenderer = spriteRenderer;
32 |
33 | // local priority 范围控制在 (0, 1),同时为了尽可能避免精度问题,this.index * 1000000
34 | spriteRenderer.priority = (Number.MAX_SAFE_INTEGER - this.index * 1000000) / Number.MAX_SAFE_INTEGER;
35 |
36 | this.width = atlasRegion.width * texture.width;
37 | this.height = atlasRegion.height * texture.height;
38 | }
39 | }
40 |
41 | destroy() {
42 | super.destroy();
43 | this.sprite = null;
44 | this.spriteRenderer = null;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/element/TextLottieElement.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Font, Engine, TextRenderer, Logger } from "@galacean/engine";
2 | import BaseLottieElement from "./BaseLottieElement";
3 | import { TypeTextKeyframe } from "../LottieResource";
4 |
5 | /**
6 | * @internal
7 | */
8 | export default class TextLottieElement extends BaseLottieElement {
9 | constructor(layer, engine?: Engine, entity?: Entity, name?: string) {
10 | super(layer);
11 |
12 | if (entity) {
13 | this.entity = entity;
14 | if (name) {
15 | this.entity.name = name;
16 | }
17 | } else {
18 | this.entity = new Entity(engine, layer.nm);
19 | }
20 | const textRenderer = this.entity.addComponent(TextRenderer);
21 | const keyframes: TypeTextKeyframe[] = layer?.t?.d?.k;
22 | if (keyframes.length === 1) {
23 | // only one frame
24 | const firstKeyframeStart = keyframes?.[0]?.s;
25 | if (firstKeyframeStart) {
26 | const { t: text, f: font, s: fontSize, fc: fontColor, lh: lineHeight } = firstKeyframeStart;
27 | // set the Font object by font
28 | textRenderer.font = Font.createFromOS(engine, font);
29 | // set the text to be displayed by text
30 | textRenderer.text = text;
31 | // set the font size by fontSize
32 | textRenderer.fontSize = fontSize;
33 | // set the text color by color
34 | textRenderer.color.set(fontColor[0], fontColor[1], fontColor[2], 1);
35 | // set line spacing via lineSpacing
36 | textRenderer.lineSpacing = lineHeight;
37 | } else {
38 | Logger.warn(`TextLottieElement: ${name}, No corresponding text data found.`);
39 | }
40 | } else {
41 | // TODO: multi keyframes
42 | Logger.warn(`TextLottieElement: multi keyframes feature is not supported in this version.`);
43 | }
44 |
45 | textRenderer.priority = (Number.MAX_SAFE_INTEGER - this.index * 1000000) / Number.MAX_SAFE_INTEGER;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "dist/main.js",
3 | "browser": "dist/browser.js",
4 | "module": "dist/module.js",
5 | "dependencies": {
6 | "bezier-easing": "^2.1.0"
7 | },
8 | "peerDependencies": {
9 | "@galacean/engine": ">=1.6.0-0"
10 | },
11 | "devDependencies": {
12 | "@babel/core": "^7.22.5",
13 | "@babel/plugin-proposal-class-properties": "^7.12.1",
14 | "@babel/plugin-proposal-decorators": "^7.12.1",
15 | "@babel/plugin-proposal-optional-chaining": "^7.12.1",
16 | "@babel/plugin-transform-object-assign": "^7.12.1",
17 | "@babel/preset-env": "^7.12.1",
18 | "@babel/preset-typescript": "^7.12.1",
19 | "@galacean/engine": ">=1.6.0-0",
20 | "@rollup/plugin-babel": "^5.2.1",
21 | "@rollup/plugin-commonjs": "^16.0.0",
22 | "@rollup/plugin-inject": "^4.0.2",
23 | "@rollup/plugin-node-resolve": "^10.0.0",
24 | "@rollup/plugin-replace": "^2.3.4",
25 | "@rollup/plugin-terser": "^0.4.3",
26 | "@swc/core": "^1.3.32",
27 | "@swc/helpers": "^0.5",
28 | "@types/dat.gui": "^0.7.10",
29 | "cross-env": "^5.2.0",
30 | "dat.gui": "^0.7.7",
31 | "react": "^16.14.0",
32 | "react-dom": "^16.14.0",
33 | "rollup": "^2.32.0",
34 | "rollup-plugin-glslify": "^1.2.0",
35 | "rollup-plugin-modify": "^3.0.0",
36 | "rollup-plugin-serve": "^1.1.0",
37 | "rollup-plugin-swc3": "^0.8.0",
38 | "typescript": "^4.8.4",
39 | "vite": "^4.3.9"
40 | },
41 | "version": "1.6.1",
42 | "description": "Lottie runtime of oasis engine",
43 | "name": "@galacean/engine-lottie",
44 | "repository": "https://github.com/galacean/engine-lottie.git",
45 | "ci": {
46 | "type": "aci",
47 | "versions": "12"
48 | },
49 | "files": [
50 | "types",
51 | "dist"
52 | ],
53 | "scripts": {
54 | "example": "vite",
55 | "test": "jest",
56 | "test-cov": "jest --coverage",
57 | "dev": "rollup -cw",
58 | "build": "npm run b:types && cross-env BUILD_TYPE=ALL rollup -c",
59 | "prepublishOnly": "npm run build",
60 | "ci": "npm run lint && npm run test-cov",
61 | "lint": "eslint src --fix --ext .ts,.tsx",
62 | "b:types": "tsc --emitDeclarationOnly"
63 | },
64 | "types": "types/index.d.ts",
65 | "packageManager": "pnpm@9.3.0"
66 | }
67 |
--------------------------------------------------------------------------------
/src/bezier.ts:
--------------------------------------------------------------------------------
1 | import BezierEasing from "bezier-easing";
2 |
3 | const defaultCurveSegments = 200;
4 | const beziers = {};
5 |
6 | /**
7 | * get a bezierEasing from real time or cache
8 | */
9 | function getBezierEasing(a: number, b: number, c: number, d: number, nm?: string): BezierEasing.EasingFunction {
10 | const str = nm || ("bez_" + a + "_" + b + "_" + c + "_" + d).replace(/\./g, "p");
11 | let bezier = beziers[str];
12 |
13 | if (bezier) {
14 | return bezier;
15 | }
16 |
17 | bezier = BezierEasing(a, b, c, d);
18 | beziers[str] = bezier;
19 |
20 | return bezier;
21 | }
22 |
23 | const storedData = {};
24 |
25 | function buildBezierData(s: number[], e: number[], to: number[], ti: number[], segments?: number) {
26 | const curveSegments: number = segments ? Math.min(segments, defaultCurveSegments) : defaultCurveSegments;
27 | const bezierName = (
28 | s[0] +
29 | "_" +
30 | s[1] +
31 | "_" +
32 | e[0] +
33 | "_" +
34 | e[1] +
35 | "_" +
36 | to[0] +
37 | "_" +
38 | to[1] +
39 | "_" +
40 | ti[0] +
41 | "_" +
42 | ti[1] +
43 | "_" +
44 | curveSegments
45 | ).replace(/\./g, "p");
46 |
47 | if (!storedData[bezierName]) {
48 | let segmentLength: number = 0;
49 | let lastPoint: number[];
50 | let points = [];
51 |
52 | for (let k = 0; k < curveSegments; k++) {
53 | const len = to.length;
54 | const point: number[] = new Array(len);
55 | const perc: number = k / (curveSegments - 1);
56 | let ptDistance: number = 0;
57 |
58 | for (let i = 0; i < len; i += 1) {
59 | const ptCoord =
60 | Math.pow(1 - perc, 3) * s[i] +
61 | 3 * Math.pow(1 - perc, 2) * perc * (s[i] + to[i]) +
62 | 3 * (1 - perc) * Math.pow(perc, 2) * (e[i] + ti[i]) +
63 | Math.pow(perc, 3) * e[i];
64 |
65 | point[i] = ptCoord;
66 |
67 | if (lastPoint) {
68 | ptDistance += Math.pow(point[i] - lastPoint[i], 2);
69 | }
70 | }
71 |
72 | ptDistance = Math.sqrt(ptDistance);
73 | segmentLength += ptDistance;
74 |
75 | points.push({
76 | partialLength: ptDistance,
77 | point
78 | });
79 |
80 | lastPoint = point;
81 | }
82 |
83 | storedData[bezierName] = {
84 | segmentLength,
85 | points
86 | };
87 | }
88 |
89 | return storedData[bezierName];
90 | }
91 |
92 | export default {
93 | buildBezierData,
94 | getBezierEasing
95 | };
96 |
--------------------------------------------------------------------------------
/src/element/BaseLottieElement.ts:
--------------------------------------------------------------------------------
1 | import { TypeLayer } from "../LottieResource";
2 | import TransformFrames from "../TransformFrames";
3 | import { Entity } from "@galacean/engine";
4 |
5 | /**
6 | * @internal
7 | */
8 | export default class BaseLottieElement {
9 | transform: TransformFrames;
10 | is3D: boolean;
11 | offsetTime: number;
12 | name: string;
13 | index: number;
14 | stretch: number = 1;
15 | parent: any = null;
16 | inPoint: any;
17 | outPoint: any;
18 | timeRemapping: any;
19 | width: number;
20 | height: number;
21 | visible: boolean = true;
22 | entity: Entity;
23 | startTime: number = 0;
24 | treeIndex: number[] = [];
25 |
26 | private childLayers = [];
27 |
28 | constructor(layer: TypeLayer) {
29 | this.is3D = !!layer.ddd;
30 | this.name = layer.nm || "";
31 | this.index = layer.index;
32 | this.timeRemapping = layer.tm;
33 | this.width = layer.w;
34 | this.height = layer.h;
35 |
36 | this.inPoint = layer.ip;
37 | this.outPoint = layer.op;
38 |
39 | if (layer.st) {
40 | this.startTime = layer.st;
41 | }
42 |
43 | this.stretch = layer.stretch || 1;
44 | this.offsetTime = layer.offsetTime || 0;
45 | if (layer.ks) {
46 | this.transform = new TransformFrames(layer.ks);
47 | }
48 | }
49 |
50 | reset() {
51 | if (this.transform) {
52 | this.transform.reset();
53 | }
54 |
55 | for (let i = 0; i < this.childLayers.length; i++) {
56 | this.childLayers[i].reset();
57 | }
58 | }
59 |
60 | update(frameNum: number = 0, isParentVisible?: boolean) {
61 | const frame = (frameNum - this.offsetTime) / this.stretch;
62 |
63 | if (isParentVisible === true) {
64 | this.visible = this.inPoint <= frame && this.outPoint >= frame;
65 | } else if (isParentVisible === false) {
66 | this.visible = false;
67 | }
68 |
69 | if (this.transform && this.visible) {
70 | this.transform.update(frame);
71 | }
72 |
73 | for (let i = 0; i < this.childLayers.length; i++) {
74 | this.childLayers[i].update(frameNum, this.visible);
75 | }
76 | }
77 |
78 | /**
79 | * add child layer
80 | */
81 | addChild(node) {
82 | node.parent = this;
83 | node.entity.parent = this.entity;
84 | this.childLayers.push(node);
85 | }
86 |
87 | destroy() {
88 | this.entity.parent = null;
89 | this.entity.destroy();
90 | this.entity = null;
91 | this.transform = null;
92 | this.parent = null;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/LottieLoader.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AssetPromise,
3 | AssetType,
4 | Engine,
5 | Entity,
6 | LoadItem,
7 | Loader,
8 | ResourceManager,
9 | Sprite,
10 | Texture2D,
11 | resourceLoader
12 | } from "@galacean/engine";
13 |
14 | import { LottieAnimation } from "./LottieAnimation";
15 | import { LottieResource, TypeRes } from "./LottieResource";
16 |
17 | class Base64Atlas {
18 | private sprites = {};
19 | private assetsPromises = [];
20 |
21 | constructor(assets, engine: Engine) {
22 | this.assetsPromises = assets.map((asset) => {
23 | return engine.resourceManager
24 | .load({
25 | url: asset.p,
26 | type: AssetType.Texture2D
27 | })
28 | .then((texture) => {
29 | const sprite = new Sprite(engine);
30 | sprite.texture = texture;
31 | this.sprites[asset.id] = sprite;
32 | });
33 | });
34 | }
35 |
36 | request() {
37 | return Promise.all(this.assetsPromises);
38 | }
39 |
40 | getSprite(id) {
41 | return this.sprites[id];
42 | }
43 | }
44 |
45 | /**
46 | * @internal
47 | */
48 | // @ts-ignore
49 | @resourceLoader("lottie", ["json"])
50 | export class LottieLoader extends Loader {
51 | // @ts-ignore
52 | load(item: LoadItem, resourceManager: ResourceManager): Promise {
53 | const { urls } = item;
54 | // @ts-ignore
55 | const jsonPromise = resourceManager._request(urls[0], { type: "json" });
56 |
57 | // atlas
58 | if (urls[1]) {
59 | const atlasPromise = resourceManager.load({
60 | url: urls[1],
61 | type: AssetType.SpriteAtlas
62 | });
63 |
64 | return AssetPromise.all([jsonPromise, atlasPromise]).then(([res, atlas]) => {
65 | const { engine } = resourceManager;
66 | const resource = new LottieResource(engine, res as TypeRes, atlas);
67 |
68 | const lottieEntity = new Entity(engine);
69 | const lottie = lottieEntity.addComponent(LottieAnimation);
70 |
71 | lottie.resource = resource;
72 |
73 | return lottieEntity;
74 | });
75 | }
76 | // base64
77 | else {
78 | return AssetPromise.all([jsonPromise]).then(([res]) => {
79 | const { engine } = resourceManager;
80 | const spriteAssets = (res as TypeRes).assets.filter((asset) => asset.p);
81 | (res as TypeRes).assets = (res as TypeRes).assets.filter((asset) => !asset.p);
82 |
83 | const atlas = new Base64Atlas(spriteAssets, engine);
84 |
85 | return atlas.request().then(() => {
86 | const resource = new LottieResource(engine, res as TypeRes, atlas);
87 |
88 | const lottieEntity = new Entity(engine);
89 | const lottie = lottieEntity.addComponent(LottieAnimation);
90 |
91 | lottie.resource = resource;
92 |
93 | return lottieEntity;
94 | });
95 | });
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/property/BaseProperty.ts:
--------------------------------------------------------------------------------
1 | import bez from "../bezier";
2 | import Expression from "../Expression";
3 |
4 | export type TypeValueProperty = {
5 | k: number;
6 | x?: Function;
7 | ix?: number;
8 | a: boolean;
9 | };
10 |
11 | type TypePoint = {
12 | partialLength: number;
13 | point: number[];
14 | };
15 |
16 | export type TypeKeyframe = {
17 | e: number[];
18 | s: number[];
19 | t: number;
20 | i?: { x: number; y: number };
21 | o?: { x: number; y: number };
22 | h?: number;
23 | ti?: number[];
24 | to?: number[];
25 | beziers?: BezierEasing.EasingFunction[];
26 | timeBezier?: BezierEasing.EasingFunction;
27 | bezierData?: { segmentLength: number; points: TypePoint[] };
28 | n?: string;
29 | };
30 |
31 | export type TypeValueKeyframedProperty = {
32 | k: TypeKeyframe[];
33 | x?: Function;
34 | ix?: number;
35 | a: boolean;
36 | };
37 |
38 | export type TypeMultiDimensionalProperty = {
39 | k: number[];
40 | x?: Function;
41 | ix?: number;
42 | a: boolean;
43 | };
44 |
45 | export type TypeMultiDimensionalKeyframedProperty = {
46 | k: TypeKeyframe[];
47 | x?: Function;
48 | ix?: number;
49 | a: boolean;
50 | };
51 | /**
52 | * basic property for animate property unit
53 | * @internal
54 | */
55 | export default class BaseProperty {
56 | mult: number;
57 | v: any;
58 |
59 | value: any;
60 | newValue: any;
61 | expression: any;
62 | animated: boolean;
63 |
64 | constructor(
65 | data:
66 | | TypeValueProperty
67 | | TypeValueKeyframedProperty
68 | | TypeMultiDimensionalProperty
69 | | TypeMultiDimensionalKeyframedProperty,
70 | mult?: number
71 | ) {
72 | this.mult = mult || 1;
73 | this.value = data.k;
74 | this.animated = data.a;
75 |
76 | if (Expression.hasSupportExpression(data)) {
77 | this.expression = Expression.getExpression(data);
78 | }
79 | }
80 |
81 | getValue(frameNum: number, i: number, keyData: TypeKeyframe, nextKeyData: TypeKeyframe) {
82 | let perc: number;
83 | const keyTime = keyData.t;
84 | const nextKeyTime = nextKeyData.t;
85 | const startValue = keyData.s[i];
86 | const endValue = (nextKeyData.s || keyData.e)[i];
87 |
88 | if (keyData.h === 1) {
89 | return startValue;
90 | }
91 |
92 | if (frameNum >= nextKeyTime) {
93 | perc = 1;
94 | } else if (frameNum < keyTime) {
95 | perc = 0;
96 | } else {
97 | let bezier: BezierEasing.EasingFunction = keyData.beziers[i];
98 |
99 | if (!bezier) {
100 | if (typeof keyData.o.x === "number") {
101 | bezier = bez.getBezierEasing(keyData.o.x, keyData.o.y, keyData.i.x, keyData.i.y);
102 | } else {
103 | bezier = bez.getBezierEasing(keyData.o.x[i], keyData.o.y[i], keyData.i.x[i], keyData.i.y[i]);
104 | }
105 | keyData.beziers[i] = bezier;
106 | }
107 |
108 | perc = bezier((frameNum - keyTime) / (nextKeyTime - keyTime));
109 | }
110 |
111 | return startValue + (endValue - startValue) * perc;
112 | }
113 |
114 | protected reset() {}
115 | }
116 |
--------------------------------------------------------------------------------
/src/property/KeyframedMultidimensionalProperty.ts:
--------------------------------------------------------------------------------
1 | import bez from "../bezier";
2 | import BaseProperty, { TypeKeyframe, TypeMultiDimensionalKeyframedProperty } from "./BaseProperty";
3 |
4 | /**
5 | * keyframed multidimensional value property
6 | */
7 | export default class KeyframedMultidimensionalProperty extends BaseProperty {
8 | private _lastPoint: number = 0;
9 | private _addedLength: number = 0;
10 | private _frames: number;
11 |
12 | constructor(data: TypeMultiDimensionalKeyframedProperty, mult: number = 1, frames?: number) {
13 | super(data, mult);
14 |
15 | let arrLen = this.value[0].s.length;
16 |
17 | // Set bezier segments according to frames, which is better for performance.
18 | if (frames) {
19 | this._frames = frames >> 0;
20 | }
21 |
22 | this.newValue = new Float32Array(arrLen);
23 | this.v = new Float32Array(arrLen);
24 | }
25 |
26 | update(frameNum: number) {
27 | if (this.expression) {
28 | frameNum = this.expression.update(frameNum);
29 | }
30 |
31 | const { value } = this;
32 | let { newValue } = this;
33 |
34 | let keyData: TypeKeyframe;
35 | let nextKeyData: TypeKeyframe;
36 |
37 | // Find current frame
38 | for (let i = 0, l = value.length - 1; i < l; i++) {
39 | keyData = value[i];
40 | nextKeyData = value[i + 1];
41 |
42 | if (nextKeyData.t > frameNum) {
43 | this._lastPoint = 0;
44 | this._addedLength = 0;
45 | break;
46 | }
47 | }
48 |
49 | if (frameNum > nextKeyData.t) {
50 | for (let i = 0, len = this.v.length; i < len; i++) {
51 | this.v[i] = this.getValue(frameNum, i, keyData, nextKeyData) * this.mult;
52 | }
53 | return;
54 | }
55 |
56 | if (keyData.to) {
57 | let nextKeyTime: number = nextKeyData.t;
58 | let keyTime: number = keyData.t;
59 |
60 | if (!keyData.bezierData) {
61 | keyData.bezierData = bez.buildBezierData(
62 | keyData.s,
63 | nextKeyData.s || keyData.e,
64 | keyData.to,
65 | keyData.ti,
66 | this._frames
67 | );
68 | }
69 |
70 | const { points, segmentLength } = keyData.bezierData;
71 |
72 | let bezier = keyData.timeBezier;
73 |
74 | // Cache time bezier easing
75 | if (!bezier) {
76 | bezier = bez.getBezierEasing(keyData.o.x, keyData.o.y, keyData.i.x, keyData.i.y, keyData.n);
77 | keyData.timeBezier = bezier;
78 | }
79 |
80 | let t = 0;
81 |
82 | if (nextKeyTime >= 0) {
83 | t = (frameNum - keyTime) / (nextKeyTime - keyTime);
84 | t = Math.min(Math.max(0, t), 1);
85 | }
86 |
87 | const percent: number = bezier(t);
88 |
89 | let distanceInLine: number = segmentLength * percent;
90 |
91 | let addedLength = this._addedLength;
92 | let lastPoint = this._lastPoint;
93 |
94 | for (let i = lastPoint, l = points.length; i < l; i++) {
95 | if (i === l - 1) {
96 | lastPoint = 0;
97 | addedLength = 0;
98 |
99 | break;
100 | }
101 |
102 | lastPoint = i;
103 |
104 | const point = points[i];
105 | const nextPoint = points[i + 1];
106 | const { partialLength } = nextPoint;
107 |
108 | if (distanceInLine >= addedLength && distanceInLine < addedLength + partialLength) {
109 | const segmentPercent: number = (distanceInLine - addedLength) / partialLength;
110 |
111 | for (let k = 0, l = point.point.length; k < l; k += 1) {
112 | newValue[k] = point.point[k] + (nextPoint.point[k] - point.point[k]) * segmentPercent;
113 | }
114 |
115 | break;
116 | }
117 |
118 | // Add partial length util the distanceInLine is between two points.
119 | addedLength += partialLength;
120 | }
121 |
122 | this._lastPoint = lastPoint;
123 | this._addedLength = addedLength;
124 | } else {
125 | if (!keyData.beziers) {
126 | keyData.beziers = [];
127 | }
128 |
129 | for (let i = 0, len = keyData.s.length; i < len; i++) {
130 | newValue[i] = this.getValue(frameNum, i, keyData, nextKeyData);
131 | }
132 | }
133 |
134 | for (let i = 0, len = this.v.length; i < len; i++) {
135 | this.v[i] = newValue[i] * this.mult;
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/Expression.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import Tools from "./tools";
3 |
4 | const EX_REG = /(loopIn|loopOut)\(([^)]+)/;
5 | const STR_REG = /["']\w+["']/;
6 |
7 | /**
8 | * Cycle
9 | * @class
10 | * @private
11 | */
12 | class Cycle {
13 | /**
14 | * Pingpong
15 | * @param {*} type Pingpong
16 | * @param {*} begin Pingpong
17 | * @param {*} end Pingpong
18 | */
19 | constructor(type, begin, end) {
20 | this.begin = begin;
21 | this.end = end;
22 | this.total = this.end - this.begin;
23 | this.type = type;
24 | }
25 |
26 | /**
27 | * progress
28 | * @param {number} progress progress
29 | * @return {number} progress
30 | */
31 | update(progress) {
32 | if (this.type === "in") {
33 | if (progress >= this.begin) return progress;
34 | return this.end - Tools.euclideanModulo(this.begin - progress, this.total);
35 | } else if (this.type === "out") {
36 | if (progress <= this.end) return progress;
37 | return this.begin + Tools.euclideanModulo(progress - this.end, this.total);
38 | }
39 | }
40 | }
41 |
42 | /**
43 | * Pingpong
44 | * @class
45 | * @private
46 | */
47 | class Pingpong {
48 | /**
49 | * Pingpong
50 | * @param {*} type Pingpong
51 | * @param {*} begin Pingpong
52 | * @param {*} end Pingpong
53 | */
54 | constructor(type, begin, end) {
55 | this.begin = begin;
56 | this.end = end;
57 | this.total = this.end - this.begin;
58 | this.type = type;
59 | }
60 |
61 | /**
62 | * progress
63 | * @param {number} progress progress
64 | * @return {number} progress
65 | */
66 | update(progress) {
67 | if ((this.type === "in" && progress < this.begin) || (this.type === "out" && progress > this.end)) {
68 | const space = progress - this.end;
69 | return this.pingpong(space);
70 | }
71 | return progress;
72 | }
73 |
74 | /**
75 | * pingpong
76 | * @param {number} space
77 | * @return {number}
78 | */
79 | pingpong(space) {
80 | const dir = Math.floor(space / this.total) % 2;
81 | if (dir) {
82 | return this.begin + Tools.euclideanModulo(space, this.total);
83 | } else {
84 | return this.end - Tools.euclideanModulo(space, this.total);
85 | }
86 | }
87 | }
88 |
89 | const FN_MAPS = {
90 | loopIn(datak, mode, offset) {
91 | const begin = datak[0].t;
92 | const last = datak.length - 1;
93 | const endIdx = Math.min(last, offset);
94 | const end = datak[endIdx].t;
95 | switch (mode) {
96 | case "cycle":
97 | return new Cycle("in", begin, end);
98 | case "pingpong":
99 | return new Pingpong("in", begin, end);
100 | default:
101 | break;
102 | }
103 | return null;
104 | },
105 | loopOut(datak, mode, offset) {
106 | const last = datak.length - 1;
107 | const beginIdx = Math.max(0, last - offset);
108 | const begin = datak[beginIdx].t;
109 | const end = datak[last].t;
110 | switch (mode) {
111 | case "cycle":
112 | return new Cycle("out", begin, end);
113 | case "pingpong":
114 | return new Pingpong("out", begin, end);
115 | default:
116 | break;
117 | }
118 | return null;
119 | }
120 | };
121 |
122 | /**
123 | * parseParams
124 | * @ignore
125 | * @param {string} pStr string
126 | * @return {array}
127 | */
128 | function parseParams(pStr) {
129 | const params = pStr.split(/\s*,\s*/);
130 | return params.map((it) => {
131 | if (STR_REG.test(it)) return it.replace(/"|'/g, "");
132 | return parseInt(it);
133 | });
134 | }
135 |
136 | /**
137 | * parseEx
138 | * @ignore
139 | * @param {string} ex string
140 | * @return {object}
141 | */
142 | function parseEx(ex) {
143 | const rs = ex.match(EX_REG);
144 | const ps = parseParams(rs[2]);
145 | return {
146 | name: rs[1],
147 | mode: ps[0],
148 | offset: ps[1]
149 | };
150 | }
151 |
152 | /**
153 | * hasSupportExpression
154 | * @ignore
155 | * @param {string} ksp string
156 | * @return {boolean}
157 | */
158 | function hasSupportExpression(ksp) {
159 | return ksp.x && EX_REG.test(ksp.x);
160 | }
161 |
162 | /**
163 | * getExpression
164 | * @ignore
165 | * @param {object} ksp ksp
166 | * @return {object}
167 | */
168 | function getExpression(ksp) {
169 | const { name, mode, offset = 0 } = parseEx(ksp.x);
170 | const _offset = offset === 0 ? ksp.k.length - 1 : offset;
171 | return FN_MAPS[name] && FN_MAPS[name](ksp.k, mode, _offset);
172 | }
173 |
174 | export default { hasSupportExpression, getExpression };
175 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 |
4 | import resolve from "@rollup/plugin-node-resolve";
5 | import commonjs from "@rollup/plugin-commonjs";
6 | import glslify from "rollup-plugin-glslify";
7 | import serve from "rollup-plugin-serve";
8 | import miniProgramPlugin from "./rollup.miniprogram.plugin";
9 | import replace from "@rollup/plugin-replace";
10 | import { swc, defineRollupSwcOption } from "rollup-plugin-swc3";
11 |
12 | const { BUILD_TYPE, NODE_ENV } = process.env;
13 |
14 | const pkgs = [
15 | {
16 | location: __dirname,
17 | pkgJson: require(path.resolve(__dirname, "package.json"))
18 | }
19 | ];
20 |
21 | // toGlobalName
22 |
23 | const extensions = [".js", ".jsx", ".ts", ".tsx"];
24 | const mainFields = NODE_ENV === "development" ? ["debug", "module", "main"] : undefined;
25 |
26 | const commonPlugins = [
27 | resolve({ extensions, preferBuiltins: true, mainFields }),
28 | glslify({
29 | include: [/\.glsl$/]
30 | }),
31 | swc(
32 | defineRollupSwcOption({
33 | include: /\.[mc]?[jt]sx?$/,
34 | exclude: /node_modules/,
35 | jsc: {
36 | loose: true,
37 | externalHelpers: true,
38 | target: "es5"
39 | },
40 | sourceMaps: true
41 | })
42 | ),
43 | commonjs(),
44 | NODE_ENV === "development"
45 | ? serve({
46 | contentBase: "packages",
47 | port: 9999
48 | })
49 | : null
50 | ];
51 |
52 | function config({ location, pkgJson }) {
53 | const input = path.join(location, "src", "index.ts");
54 | const dependencies = Object.assign(pkgJson.peerDependencies ?? {});
55 | const commonExternal = Object.keys(dependencies);
56 | commonPlugins.push(
57 | replace({
58 | preventAssignment: true,
59 | __buildVersion: pkgJson.version
60 | })
61 | );
62 |
63 | return {
64 | umd: () => {
65 | let file = path.join(location, "dist", "browser.js");
66 | const plugins = [...commonPlugins];
67 | return {
68 | input,
69 | external: commonExternal,
70 | output: [
71 | {
72 | file,
73 | name: "Galacean.Lottie",
74 | format: "umd",
75 | globals: {
76 | "@galacean/engine": "Galacean"
77 | }
78 | }
79 | ],
80 | plugins
81 | };
82 | },
83 | mini: () => {
84 | const external = commonExternal
85 | .concat("@galacean/engine-miniprogram-adapter")
86 | .map((name) => `${name}/dist/miniprogram`);
87 | const plugins = [...commonPlugins, ...miniProgramPlugin];
88 | return {
89 | input,
90 | output: [
91 | {
92 | format: "cjs",
93 | file: path.join(location, "dist/miniprogram.js"),
94 | sourcemap: false
95 | }
96 | ],
97 | external,
98 | plugins
99 | };
100 | },
101 | module: () => {
102 | const plugins = [...commonPlugins];
103 | return {
104 | input,
105 | external: commonExternal,
106 | output: [
107 | {
108 | file: path.join(location, pkgJson.module),
109 | format: "es",
110 | sourcemap: true
111 | },
112 | {
113 | file: path.join(location, pkgJson.main),
114 | sourcemap: true,
115 | format: "commonjs"
116 | }
117 | ],
118 | plugins
119 | };
120 | }
121 | };
122 | }
123 |
124 | async function makeRollupConfig({ type, compress = true, visualizer = true, ..._ }) {
125 | return config({ ..._ })[type](compress, visualizer);
126 | }
127 |
128 | let promises = [];
129 |
130 | switch (BUILD_TYPE) {
131 | case "UMD":
132 | promises.push(...getUMD());
133 | break;
134 | case "MODULE":
135 | promises.push(...getModule());
136 | break;
137 | case "MINI":
138 | promises.push(...getMini());
139 | break;
140 | case "ALL":
141 | promises.push(...getAll());
142 | break;
143 | default:
144 | break;
145 | }
146 |
147 | function getUMD() {
148 | const configs = [...pkgs];
149 | return configs.map((config) => makeRollupConfig({ ...config, type: "umd" }));
150 | }
151 |
152 | function getModule() {
153 | const configs = [...pkgs];
154 | return configs.map((config) => makeRollupConfig({ ...config, type: "module" }));
155 | }
156 |
157 | function getMini() {
158 | const configs = [...pkgs];
159 | return configs.map((config) => makeRollupConfig({ ...config, type: "mini" }));
160 | }
161 |
162 | function getAll() {
163 | return [...getModule(), ...getMini(), ...getUMD()];
164 | }
165 |
166 | export default Promise.all(promises);
167 |
--------------------------------------------------------------------------------
/src/TransformFrames.ts:
--------------------------------------------------------------------------------
1 | import ValueProperty from "./property/ValueProperty";
2 | import MultiDimensionalProperty from "./property/MultiDimensionalProperty";
3 | import KeyframedValueProperty from "./property/KeyframedValueProperty";
4 | import KeyframedMultidimensionalProperty from "./property/KeyframedMultidimensionalProperty";
5 | import { TypeMultiDimensionalKeyframedProperty, TypeValueKeyframedProperty } from "./property/BaseProperty";
6 |
7 | type KeyFrames = {
8 | a: TypeMultiDimensionalKeyframedProperty;
9 | p: TypeMultiDimensionalKeyframedProperty;
10 | s: TypeMultiDimensionalKeyframedProperty;
11 | or?: TypeMultiDimensionalKeyframedProperty;
12 | o: TypeValueKeyframedProperty;
13 | r: TypeValueKeyframedProperty;
14 | rx?: TypeValueKeyframedProperty;
15 | ry?: TypeValueKeyframedProperty;
16 | rz?: TypeValueKeyframedProperty;
17 | };
18 |
19 | /**
20 | * transform property origin from tr or ks
21 | */
22 | export default class TransformFrames {
23 | p: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
24 | x: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
25 | y: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
26 | z: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
27 | a: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
28 | s: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
29 | or: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
30 | r: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
31 | o: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
32 | rx: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
33 | ry: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
34 | rz: ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty;
35 | private properties = [];
36 | private autoOrient: boolean = false;
37 |
38 | static create(
39 | data,
40 | type = 0,
41 | mult = 1
42 | ): ValueProperty | MultiDimensionalProperty | KeyframedValueProperty | KeyframedMultidimensionalProperty {
43 | if (!data.k.length) {
44 | return new ValueProperty(data, mult);
45 | } else if (typeof data.k[0] === "number") {
46 | return new MultiDimensionalProperty(data, mult);
47 | } else {
48 | if (type) {
49 | return new KeyframedMultidimensionalProperty(data, mult, data.k[data.k.length - 1].t - data.k[0].t);
50 | } else {
51 | return new KeyframedValueProperty(data, mult);
52 | }
53 | }
54 | }
55 |
56 | constructor(data: KeyFrames) {
57 | const { create } = TransformFrames;
58 |
59 | if (data.p.k) {
60 | this.p = create(data.p, 1);
61 | this.properties.push(this.p);
62 | } else {
63 | if (data.p.x) {
64 | this.x = create(data.p.x, 1);
65 | this.properties.push(this.x);
66 | }
67 |
68 | // @ts-ignore
69 | if (data.p.y) {
70 | // @ts-ignore
71 | this.y = create(data.p.y, 1);
72 | this.properties.push(this.y);
73 | }
74 |
75 | // @ts-ignore
76 | if (data.p.z) {
77 | // @ts-ignore
78 | this.z = create(data.p.z, 1);
79 | this.properties.push(this.z);
80 | }
81 | }
82 |
83 | this.a = create(data.a, 1);
84 | this.properties.push(this.a);
85 |
86 | this.s = create(data.s, 1, 0.01);
87 | this.properties.push(this.s);
88 |
89 | this.o = create(data.o, 0, 0.01);
90 | this.properties.push(this.o);
91 |
92 | // 2d rotation
93 | if (data.r) {
94 | this.r = create(data.r, 0);
95 | this.properties.push(this.r);
96 | }
97 | // 3d rotation
98 | else if (data.rx || data.ry || data.rz) {
99 | if (data.rx) {
100 | this.rx = create(data.rx, 0);
101 | this.properties.push(this.rx);
102 | }
103 | if (data.ry) {
104 | this.ry = create(data.ry, 0);
105 | this.properties.push(this.ry);
106 | }
107 | if (data.rz) {
108 | this.rz = create(data.rz, 0);
109 | this.properties.push(this.rz);
110 | }
111 | } else if (data.or) {
112 | this.or = create(data.or, 1);
113 | this.properties.push(this.or);
114 | }
115 |
116 | if (!this.properties.length) {
117 | this.update();
118 | }
119 | }
120 |
121 | reset() {
122 | for (let i = 0, len = this.properties.length; i < len; i++) {
123 | this.properties[i].reset();
124 | }
125 | }
126 |
127 | update(frameNum: number = 0) {
128 | const len = this.properties.length;
129 |
130 | for (let i = 0; i < len; i++) {
131 | this.properties[i].update(frameNum);
132 | }
133 |
134 | if (this.autoOrient) {
135 | // TODO
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/LottieResource.ts:
--------------------------------------------------------------------------------
1 | import { Engine, EngineObject } from "@galacean/engine";
2 |
3 | export type TypeLayer = {
4 | ddd: number;
5 | sr: number;
6 | st: number;
7 | nm: string;
8 | op: number;
9 | ks: any;
10 | ao: number;
11 | ip: any;
12 | ind: number;
13 | refId: string;
14 | tm: any;
15 | w: number;
16 | h: number;
17 | fr: number;
18 | layers: TypeLayer[];
19 |
20 | // todo: delete
21 | // 继承了时间条的时序与渲染关系
22 | offsetTime: number;
23 | stretch: number;
24 | index: number;
25 |
26 | t?: TypeText;
27 | };
28 |
29 | export type TypeAnimationClip = {
30 | name: string;
31 | start: number;
32 | end: number;
33 | auto: boolean;
34 | };
35 |
36 | export type TypeRes = {
37 | v: string;
38 | nm: string;
39 | ddd: number;
40 | fr: number;
41 | w: number;
42 | h: number;
43 | ip: number;
44 | op: number;
45 | layers: TypeLayer[];
46 | assets: any[];
47 | lolitaAnimations?: TypeAnimationClip[];
48 | };
49 |
50 | export type TypeText = {
51 | d: {
52 | k: TypeTextKeyframe[];
53 | };
54 | };
55 |
56 | // field explanation:https://lottiefiles.github.io/lottie-docs/text/#text-document
57 | export type TypeTextKeyframe = {
58 | s: {
59 | t: string;
60 | f: string;
61 | s: number;
62 | fc: number[];
63 | lh: number;
64 | };
65 | t: number;
66 | };
67 |
68 | /**
69 | * @internal
70 | */
71 | export class LottieResource extends EngineObject {
72 | duration: number;
73 | timePerFrame: number;
74 | inPoint: number;
75 | outPoint: number;
76 | height: number;
77 | width: number;
78 | layers: TypeLayer[];
79 | comps: any[];
80 | atlas: any;
81 | name: string;
82 | clips: { [name: string]: TypeAnimationClip };
83 | refCount: number = 0;
84 |
85 | constructor(engine: Engine, res: TypeRes, atlas: any) {
86 | super(engine);
87 |
88 | this.timePerFrame = 1000 / res.fr;
89 | this.duration = Math.floor(res.op - res.ip);
90 | this.width = res.w;
91 | this.height = res.h;
92 | this.inPoint = res.ip;
93 | this.outPoint = res.op;
94 | this.atlas = atlas;
95 | this.layers = res.layers;
96 | this.comps = res.assets;
97 | this.name = res.nm;
98 | this.clips = {};
99 |
100 | const compsMap = {};
101 | const { comps } = this;
102 |
103 | if (comps) {
104 | for (let i = 0, l = comps.length; i < l; i++) {
105 | const comp = comps[i];
106 | if (comp.id) {
107 | compsMap[comp.id] = comp;
108 | }
109 | }
110 | }
111 |
112 | this._buildTree(this.layers, compsMap);
113 |
114 | if (res.lolitaAnimations) {
115 | this._parseAnimations(res.lolitaAnimations);
116 | }
117 | }
118 |
119 | setClips(v: TypeAnimationClip[]) {
120 | this.clips = {};
121 | this._parseAnimations(v);
122 | }
123 |
124 | private _parseAnimations(clips: TypeAnimationClip[]) {
125 | clips.forEach((clip) => {
126 | this.clips[clip.name] = { ...clip };
127 | });
128 | }
129 |
130 | /**
131 | * 在构建树结构的同时,继承合成的时间条关系
132 | * @param layers
133 | * @param compsMap
134 | * @param startTime - 这条合成的 offsetTime
135 | * @param stretch - 这条合成的 stretch
136 | * @param indStart - 这条合成的基础 ind
137 | * @param indFactor - 这条合成的 ind 缩放因子
138 | */
139 | private _buildTree(
140 | layers,
141 | compsMap,
142 | startTime: number = 0,
143 | stretch: number = 1,
144 | indStart: number = 0,
145 | indFactor: number = 1
146 | ) {
147 | const layersMap = {};
148 |
149 | for (let i = 0, l = layers.length; i < l; i++) {
150 | const layer = layers[i];
151 | layersMap[layer.ind ?? i] = layer;
152 | }
153 |
154 | for (let i = layers.length - 1; i >= 0; i--) {
155 | const layer = layers[i];
156 | const { refId, parent } = layer;
157 | layer.offsetTime = startTime;
158 | layer.stretch = stretch;
159 | layer.index = (layer.ind ?? i) * indFactor + indStart;
160 | if (parent) {
161 | if (!layersMap[parent].layers) {
162 | layersMap[parent].layers = [];
163 | }
164 |
165 | layersMap[parent].layers.push(layer);
166 | layers.splice(i, 1);
167 | }
168 |
169 | if (refId && compsMap[refId]) {
170 | const refLayers = [];
171 | // deep clone the layers in comp asset
172 | for (let j = 0; j < compsMap[refId].layers.length; j++) {
173 | refLayers.push(this._deepClone(compsMap[refId].layers[j]));
174 | }
175 | const offsetTime = (layer.offsetTime || 0) + (layer.st || 0);
176 | const stretch = (layer.stretch || 1) * (layer.sr || 1);
177 | const lastIndex = refLayers.length - 1;
178 | const compIndFactor = (indFactor / ((refLayers[lastIndex].ind ?? lastIndex) + 1)) * indFactor;
179 | this._buildTree(refLayers, compsMap, offsetTime, stretch, layer.index, compIndFactor);
180 | if (layer.layers) {
181 | layer.layers.push(...refLayers);
182 | } else {
183 | layer.layers = [...refLayers];
184 | }
185 | }
186 | }
187 | }
188 |
189 | private _deepClone(from: Object): Object {
190 | let out = Array.isArray(from) ? [...from] : { ...from };
191 | Reflect.ownKeys(out).map((key) => {
192 | out[key] = this._isObject(from[key]) ? this._deepClone(from[key]) : from[key];
193 | });
194 | return out;
195 | }
196 |
197 | private _isObject(obj: Object) {
198 | return (typeof obj === "object" || typeof obj === "function") && typeof obj !== null;
199 | }
200 |
201 | destroy(): void {
202 | // Maybe atlas is Base64Atlas
203 | this.atlas.destroy?.();
204 | this.atlas = null;
205 | this.layers = null;
206 | this.clips = null;
207 | this.comps = null;
208 |
209 | super.destroy();
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/example/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { LottieAnimation, LottieResource } from "../src";
3 | import { Camera, Entity, Layer, Vector3, WebGLEngine } from "@galacean/engine";
4 | import * as dat from "dat.gui";
5 | import "./App.css";
6 |
7 | async function init() {
8 | // gui
9 | const gui = new dat.GUI({ name: "My GUI" });
10 |
11 | const demos = {
12 | 贝壳红包: [
13 | "https://gw.alipayobjects.com/os/bmw-prod/01e685be-4090-4e9c-bdef-f437038a4a78.json",
14 | "https://gw.alipayobjects.com/os/bmw-prod/07dcd051-b3d2-4f36-9459-725ae66d9cbf.atlas"
15 | ],
16 | "3d": [
17 | "https://gw.alipayobjects.com/os/bmw-prod/70bed2d5-7284-44bf-9df6-638f66945ffd.json",
18 | "https://gw.alipayobjects.com/os/bmw-prod/a2853204-2d4a-48e5-9cb7-b89de8dcc7bf.atlas"
19 | ],
20 | 芝麻工作证: [
21 | "https://gw.alipayobjects.com/os/bmw-prod/32420b26-7305-46ef-bfa1-48c5d6b2a45e.json",
22 | "https://gw.alipayobjects.com/os/bmw-prod/3c054399-2b10-4d68-96f7-0973e3d9ace6.atlas"
23 | ],
24 | 花花卡: [
25 | "https://gw.alipayobjects.com/os/bmw-prod/b46be138-e48b-4957-8071-7229661aba53.json",
26 | "https://gw.alipayobjects.com/os/bmw-prod/6447fc36-db32-4834-9579-24fe33534f55.atlas"
27 | ],
28 | 灯牌: [
29 | "https://gw.alipayobjects.com/os/bmw-prod/bbf83713-c23f-4981-8b8d-241d905fc3bf.json",
30 | "https://gw.alipayobjects.com/os/bmw-prod/d9b42223-b1ae-4f51-b489-75b2f36a2b2d.atlas"
31 | ],
32 | 频道氛围1: [
33 | "https://gw.alipayobjects.com/os/OasisHub/bbbbd4a1-6356-46f8-8c5e-eab55b5a137a/lottie.json",
34 | "https://gw.alipayobjects.com/os/OasisHub/c0730585-4f56-4bf8-9dca-4d027b4826dd/lottie.atlas"
35 | ],
36 | 频道氛围2: [
37 | "https://gw.alipayobjects.com/os/OasisHub/cb3c7c17-d0c0-4ba3-bbb6-bb009ccd6f96/lottie.json",
38 | "https://gw.alipayobjects.com/os/OasisHub/62c1e950-49c7-4428-a47d-d628696330ea/lottie.atlas"
39 | ],
40 | 频道氛围3: [
41 | "https://gw.alipayobjects.com/os/OasisHub/58bce243-16e0-45f4-b1e7-3475e03b8f7a/lottie.json",
42 | "https://gw.alipayobjects.com/os/OasisHub/01469a2c-b8ed-4f13-9886-b235a2e326b0/lottie.atlas"
43 | ],
44 | 小狮子: [
45 | "https://gw.alipayobjects.com/os/bmw-prod/9ad65a42-9171-47ab-9218-54cf175f6201.json",
46 | "https://gw.alipayobjects.com/os/bmw-prod/90779ce2-50f1-4780-ae74-725083eba852.atlas"
47 | ],
48 | 宝箱: [
49 | "https://gw.alipayobjects.com/os/bmw-prod/84c13df1-567c-4a67-aa1e-c378ee698c55.json",
50 | "https://gw.alipayobjects.com/os/bmw-prod/965eb2ca-ee3c-4c54-a502-7fdc0673f1d7.atlas"
51 | ],
52 | 大桔: [
53 | "https://gw.alipayobjects.com/os/bmw-prod/da290d57-5d7a-4169-bfa3-b61e3dbe34f9.json",
54 | "https://gw.alipayobjects.com/os/bmw-prod/7e1416d6-64d6-4649-8bc1-fefce8d45adc.atlas"
55 | ],
56 | 年年有鱼: [
57 | "https://gw.alipayobjects.com/os/OasisHub/14a29798-ea24-42db-93be-462be45f2a85/lottie.json",
58 | "https://gw.alipayobjects.com/os/OasisHub/b60595c5-3d59-42a8-8bf9-f4323c704189/lottie.atlas"
59 | ],
60 | base64: [
61 | "https://gw.alipayobjects.com/os/bmw-prod/6521d990-6218-4308-aa98-bd7514b9e18f.json"
62 | // 'https://gw.alipayobjects.com/os/finxbff/lolita/97cecb8f-ff16-4fe1-8344-3b8f04ac3713/lottie.json'
63 | // 'https://gw.alipayobjects.com/os/OasisHub/d9d330ca-26fe-45c4-8127-d59a2620dc15/data.json'
64 | // 'https://gw.alipayobjects.com/os/OasisHub/62ee911f-04ac-414c-b100-a18bae585f35/data.json'
65 | // 'https://gw.alipayobjects.com/os/OasisHub/13a05f71-8e93-4569-847f-eb7fbd8dca2d/data.json'
66 | ],
67 | "818": [
68 | "https://gw.alipayobjects.com/os/bmw-prod/3cb395d8-5196-4382-9459-e4379f9414f3.json",
69 | "https://gw.alipayobjects.com/os/bmw-prod/4bd3f75c-ce9f-4d67-bf28-adbc65fad8b2.atlas"
70 | ],
71 | 碎片: ["https://mdn.alipayobjects.com/huamei_w1o8la/afts/file/A*GxWPRbeur0MAAAAAAAAAAAAADsB_AQ"],
72 | "福袋(金额文字)": [
73 | "https://mdn.alipayobjects.com/marketing/afts/file/A*LmhJSp_Owt8AAAAAAAAAAAAADviWAQ",
74 | "https://mdn.alipayobjects.com/marketing/afts/file/A*XnCbR7BvZGAAAAAAAAAAAAAADviWAQ"
75 | ]
76 | };
77 |
78 | let curLottie: LottieAnimation | null;
79 |
80 | let lastLottieEntity: Entity;
81 | const reloadLottie = async (v: string) => {
82 | const lottieEntity = await engine.resourceManager.load({
83 | urls: demos[v],
84 | type: "lottie"
85 | });
86 |
87 | if (lastLottieEntity) {
88 | lastLottieEntity.destroy();
89 | }
90 |
91 | root.addChild(lottieEntity);
92 | lastLottieEntity = lottieEntity;
93 |
94 | const lottie: LottieAnimation = lottieEntity.getComponent(LottieAnimation);
95 | lottie.isLooping = true;
96 | lottie.layer = Layer.Layer1;
97 | // lottie.speed = 0.5;
98 | // destroy resource if need not clone
99 | lottie.resource.destroy();
100 | lottie.play();
101 |
102 | // lottieEntity.clone();
103 |
104 | // test destroy
105 | // setTimeout(() => {
106 | // console.log('destroy')
107 | // lottieEntity.destroy();
108 | // }, 2000);
109 |
110 | return lottie;
111 | };
112 |
113 | let alphaController: dat.GUIController;
114 |
115 | gui.add({ name: "base64" }, "name", Object.keys(demos)).onChange(async (v) => {
116 | curLottie = await reloadLottie(v);
117 | alphaController.setValue(curLottie.alpha);
118 | });
119 |
120 | alphaController = gui.add({ alpha: 1 }, "alpha", 0, 1).onChange((v) => {
121 | if (curLottie) {
122 | curLottie.alpha = v;
123 | }
124 | });
125 |
126 | // gui add button
127 | gui
128 | .add(
129 | {
130 | play: () => {
131 | if (curLottie) {
132 | if (curLottie.isPlaying) {
133 | curLottie.pause();
134 | } else {
135 | curLottie.play();
136 | }
137 | }
138 | }
139 | },
140 | "play"
141 | )
142 | .name("Play");
143 |
144 | const engine = await WebGLEngine.create({ canvas: "canvas" });
145 |
146 | engine.canvas.resizeByClientSize();
147 |
148 | const root = engine.sceneManager.activeScene.createRootEntity();
149 |
150 | const cameraEntity = root.createChild("camera");
151 | const camera = cameraEntity.addComponent(Camera);
152 | // camera.cullingMask = Layer.Layer1;
153 | cameraEntity.transform.setPosition(0, 0, 10);
154 | cameraEntity.transform.lookAt(new Vector3(0, 0, 0));
155 | // cameraEntity.addComponent(OrbitControl);
156 |
157 | // curLottie = await reloadLottie('base64');
158 | // lastLottieEntity = curLottie.entity;
159 | const lottieResource = await engine.resourceManager.load({
160 | url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*vGadR5BXK6kAAAAAAAAAAAAADkp5AQ/CCB.json",
161 | type: "EditorLottie"
162 | });
163 | const lottieResource1 = await engine.resourceManager.load({
164 | url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*hBHUQJOpmxUAAAAAAAAAAAAADkp5AQ/CMB.json",
165 | type: "EditorLottie"
166 | });
167 | const lottieResource2 = await engine.resourceManager.load({
168 | url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*9RpGQYQ7FIUAAAAAAAAAAAAADkp5AQ/SPABANK.json",
169 | type: "EditorLottie"
170 | });
171 | const lottieEntity = root.createChild('lottie');
172 | const lottieComponent = lottieEntity.addComponent(LottieAnimation);
173 | lottieComponent.resource = lottieResource;
174 | lottieComponent.isLooping = true;
175 | lottieComponent.speed = 1;
176 | lottieComponent.play();
177 |
178 | setTimeout(() => {
179 | lottieComponent.resource = lottieResource1;
180 | setTimeout(async () => {
181 | lottieComponent.resource = await engine.resourceManager.load({
182 | url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*vGadR5BXK6kAAAAAAAAAAAAADkp5AQ/CCB.json",
183 | type: "EditorLottie"
184 | });;
185 | }, 1000);
186 | }, 1000);
187 |
188 | engine.run();
189 | }
190 |
191 | function App() {
192 | useEffect(() => {
193 | init();
194 | }, []);
195 |
196 | return ;
197 | }
198 |
199 | export default App;
200 |
--------------------------------------------------------------------------------
/src/LottieAnimation.ts:
--------------------------------------------------------------------------------
1 | import CompLottieElement from "./element/CompLottieElement";
2 | import SpriteLottieElement from "./element/SpriteLottieElement";
3 | import TextLottieElement from "./element/TextLottieElement";
4 | import Tools from "./tools";
5 | import { Script, Vector2, ignoreClone, Entity, Layer, Engine, Renderer } from "@galacean/engine";
6 | import { LottieResource, TypeAnimationClip } from "./LottieResource";
7 | import BaseLottieLayer from "./element/BaseLottieElement";
8 |
9 | export class LottieAnimation extends Script {
10 | private static _pivotVector: Vector2 = new Vector2();
11 | private static _tempRenderers: Array = [];
12 |
13 | /** The number of units in world space that correspond to one pixel in the sprite. */
14 | /** Repeat times of the animation. */
15 | repeats: number = 0;
16 | /** whether the animation loop or not. */
17 | isLooping: boolean = false;
18 | /** whether the animation play back and forth */
19 | isAlternate: boolean = false;
20 | /** The direction of animation, 1 means play for */
21 | direction: 1 | -1 = 1;
22 | speed: number = 1;
23 | // @ts-ignore
24 | pixelsPerUnit: number = Engine._pixelsPerUnit;
25 |
26 | private _alpha: number = 1;
27 |
28 | private _width: number;
29 | private _height: number;
30 |
31 | private _resource: LottieResource;
32 | private _priority: number = 0;
33 | private _priorityDirty: boolean = true;
34 | private _layer: Layer = Layer.Layer0;
35 | private _layerDirty: boolean = true;
36 | private _clip: TypeAnimationClip;
37 | private _autoPlay: boolean = false;
38 |
39 | @ignoreClone
40 | private _clipEndCallbacks: Object = {};
41 | @ignoreClone
42 | private _isPlaying: boolean = false;
43 | @ignoreClone
44 | private _curFrame: number = 0;
45 | @ignoreClone
46 | private _frame: number = 0;
47 | @ignoreClone
48 | private _root: CompLottieElement = null;
49 | @ignoreClone
50 | private _elements: BaseLottieLayer[];
51 |
52 | set resource(value: LottieResource) {
53 | if (this._resource === value) {
54 | return;
55 | }
56 |
57 | if (this._resource) {
58 | this.pause();
59 | this._destroy();
60 | this._resource.refCount--;
61 | if (this._resource.refCount <= 0) {
62 | this._resource.destroy();
63 | }
64 | }
65 |
66 | this._resource = value;
67 | if (value) {
68 | value.refCount++;
69 | this._width = value.width;
70 | this._height = value.height;
71 |
72 | this._createElements(value);
73 | this._priorityDirty = true;
74 | this._layerDirty = true;
75 | }
76 |
77 | // update the first frame
78 | this._curFrame = 0;
79 | this.play();
80 | this.onUpdate(0);
81 |
82 | if (!this.autoPlay) {
83 | this.pause();
84 | }
85 | }
86 |
87 | get resource(): LottieResource {
88 | return this._resource;
89 | }
90 |
91 | set priority(value: number) {
92 | if (this._priority !== value) {
93 | this._priority = value;
94 | this._priorityDirty = true;
95 | }
96 | }
97 |
98 | get priority(): number {
99 | return this._priority;
100 | }
101 |
102 | set layer(value: Layer) {
103 | if (this._layer !== value) {
104 | this._layer = value;
105 | this._layerDirty = true;
106 | }
107 | }
108 |
109 | get layer(): Layer {
110 | return this._layer;
111 | }
112 |
113 | /**
114 | * global alpha
115 | */
116 | set alpha(value: number) {
117 | // update in updateElement
118 | if (this._alpha !== value) {
119 | this._alpha = value;
120 |
121 | if (!this.isPlaying) {
122 | this.play();
123 | this.onUpdate(0);
124 | this.pause();
125 | }
126 | }
127 | }
128 |
129 | get alpha(): number {
130 | return this._alpha;
131 | }
132 |
133 | set autoPlay(value: boolean) {
134 | this._autoPlay = value;
135 |
136 | if (value) {
137 | this.play();
138 | }
139 | }
140 |
141 | get autoPlay(): boolean {
142 | return this._autoPlay;
143 | }
144 |
145 | get frame(): number {
146 | return this._frame;
147 | }
148 |
149 | get isPlaying(): boolean {
150 | return this._isPlaying;
151 | }
152 |
153 | /**
154 | * Play the lottie animation
155 | */
156 | play(name?: string): Promise {
157 | if (name) {
158 | const clip = this.resource.clips[name];
159 | this._clip = clip;
160 | } else {
161 | this._clip = null;
162 | }
163 |
164 | this._isPlaying = true;
165 | this._frame = this._curFrame;
166 |
167 | return new Promise((resolve) => {
168 | if (name) {
169 | this._clipEndCallbacks[name] = resolve;
170 | } else {
171 | this._clipEndCallbacks["ALL"] = resolve;
172 | }
173 | });
174 | }
175 |
176 | /**
177 | * Pause the lottie animation
178 | */
179 | pause(): void {
180 | this._isPlaying = false;
181 | this._curFrame = this._frame;
182 | }
183 |
184 | stop() {
185 | this._isPlaying = false;
186 | this._curFrame = 0;
187 | this._frame = 0;
188 | }
189 |
190 | private _setLayer(layer: Layer, entity?: Entity) {
191 | if (!entity) {
192 | entity = this.entity;
193 | }
194 |
195 | entity.layer = layer;
196 | const children = entity.children;
197 |
198 | for (let i = children.length - 1; i >= 0; i--) {
199 | const child = children[i];
200 | child.layer = layer;
201 | this._setLayer(layer, child);
202 | }
203 | }
204 |
205 | private _createLayerElements(layers, elements, parent, isCloned?: boolean) {
206 | if (!layers) return;
207 |
208 | for (let i = 0, l = layers.length; i < l; i++) {
209 | const layer = layers[i];
210 | let element = null;
211 |
212 | if (layer.td !== undefined) continue;
213 |
214 | const treeIndex = parent.treeIndex.concat(i);
215 |
216 | let childEntity: Entity = isCloned && this._findEntityInTree(treeIndex);
217 |
218 | switch (layer.ty) {
219 | case 0:
220 | element = new CompLottieElement(layer, this.engine, childEntity, layer.id);
221 | break;
222 |
223 | case 2:
224 | element = new SpriteLottieElement(layer, this._resource.atlas, this.entity, childEntity);
225 |
226 | break;
227 |
228 | case 3:
229 | if (layer?.ks?.o?.k === 0) {
230 | layer.ks.o.k = 100;
231 | }
232 |
233 | element = new CompLottieElement(layer, this.engine, childEntity, layer.id);
234 |
235 | break;
236 |
237 | case 5:
238 | element = new TextLottieElement(layer, this.engine, childEntity, layer.id);
239 |
240 | break;
241 | }
242 |
243 | if (element) {
244 | element.treeIndex = treeIndex;
245 |
246 | elements.push(element);
247 | parent.addChild(element);
248 | if (layer.layers) {
249 | this._createLayerElements(layer.layers, elements, element, isCloned);
250 | }
251 | }
252 | }
253 | }
254 |
255 | private _findEntityInTree(treeIndex) {
256 | let childEntity: Entity;
257 |
258 | for (let i = 0, l = treeIndex.length; i < l; i++) {
259 | const index = treeIndex[i];
260 |
261 | if (childEntity) {
262 | childEntity = childEntity.children[index];
263 | } else {
264 | childEntity = this.entity.children[index];
265 | }
266 | }
267 |
268 | return childEntity;
269 | }
270 |
271 | private _createElements(value, isCloned?: boolean) {
272 | const root = new CompLottieElement(value, this.engine, this.entity);
273 | this._root = root;
274 |
275 | const { layers } = root;
276 |
277 | const elements = [];
278 |
279 | this._createLayerElements(layers, elements, root, isCloned);
280 |
281 | this._elements = elements;
282 | }
283 |
284 | private _updateElements(correctedFrame: number): void {
285 | this._root.update(correctedFrame);
286 |
287 | const elements = this._elements;
288 |
289 | for (let i = 0, l = elements.length; i < l; i++) {
290 | const layer = elements[i];
291 |
292 | this._updateElement(layer);
293 | }
294 | }
295 |
296 | private _updateElement(layer: T) {
297 | // @ts-ignore
298 | const { transform, entity, sprite, spriteRenderer, parent, width, height } = layer;
299 | const entityTransform = entity.transform;
300 | const a = transform.a.v;
301 | const s = transform.s.v;
302 | let o = transform.o.v;
303 | const { pixelsPerUnit } = this;
304 |
305 | let x: number = 0,
306 | y: number = 0,
307 | z: number = 0;
308 |
309 | if (transform.p) {
310 | const p = transform.p.v;
311 | x = p[0] ?? 0;
312 | y = p[1] ?? 0;
313 | z = p[2] ?? 0;
314 | } else {
315 | if (transform.x) {
316 | x = transform.x.v;
317 | }
318 |
319 | if (transform.y) {
320 | y = transform.y.v;
321 | }
322 |
323 | if (transform.z) {
324 | z = transform.z.v;
325 | }
326 | }
327 |
328 | let rx = 0;
329 | let ry = 0;
330 | let rz = 0;
331 |
332 | if (!layer.visible) {
333 | entity.isActive = layer.visible;
334 | return;
335 | }
336 |
337 | // 2d rotation
338 | if (transform.r) {
339 | rz = transform.r.v;
340 | }
341 | // 3d rotation
342 | else if (transform.rx || transform.ry || transform.rz) {
343 | rx = transform.rx ? transform.rx.v : 0;
344 | ry = transform.ry ? transform.ry.v : 0;
345 | rz = transform.rz ? transform.rz.v : 0;
346 | } else if (transform.or) {
347 | const { v } = transform.or;
348 | rx = v[0];
349 | ry = v[1];
350 | rz = v[2];
351 | }
352 |
353 | // parent opacity
354 | if (parent?.transform?.o) {
355 | o *= parent?.transform.o.v;
356 | }
357 |
358 | if (sprite) {
359 | // update color of sprite
360 | const { r, g, b } = spriteRenderer.color;
361 | spriteRenderer.color.set(r, g, b, o * this._alpha);
362 |
363 | // update pixels per unit of sprite
364 | sprite.pixelsPerUnit = pixelsPerUnit;
365 |
366 | // update pivot of sprite
367 | sprite.pivot = LottieAnimation._pivotVector.set(a[0] / width, (height - a[1]) / height);
368 | }
369 |
370 | entity.isActive = layer.visible;
371 |
372 | // scale
373 | entityTransform.setScale(s[0], s[1], s[2]);
374 | entityTransform.setRotation(rx, ry, -rz);
375 |
376 | // anchor
377 | if (parent?.transform?.a) {
378 | entityTransform.setPosition(
379 | (x - parent.transform.a.v[0]) / pixelsPerUnit,
380 | (-y + parent.transform.a.v[1]) / pixelsPerUnit,
381 | z / pixelsPerUnit
382 | );
383 | } else {
384 | entityTransform.setPosition(
385 | (x - this._width / 2) / pixelsPerUnit,
386 | (-y + this._height / 2) / pixelsPerUnit,
387 | z / pixelsPerUnit
388 | );
389 | }
390 | }
391 |
392 | private _resetElements() {
393 | const elements = this._elements;
394 |
395 | for (let i = 0, l = elements.length; i < l; i++) {
396 | elements[i].reset();
397 | }
398 | }
399 |
400 | /**
401 | * @override
402 | */
403 | onUpdate(deltaTime: number): void {
404 | if (!this._isPlaying || !this._resource) {
405 | return null;
406 | }
407 |
408 | if (this._priorityDirty) {
409 | this._priorityDirty = false;
410 | const renderers = LottieAnimation._tempRenderers;
411 | renderers.length = 0;
412 | this.entity.getComponentsIncludeChildren(Renderer, renderers);
413 | // the diff of global priority
414 | let priorityDiff = 0;
415 | for (let i = 0, l = renderers.length; i < l; ++i) {
416 | const renderer = renderers[i];
417 | if (i === 0) {
418 | // this._priority represent global priority,Math.floor(renderer.priority) get current global priority
419 | priorityDiff = this._priority - Math.floor(renderer.priority);
420 | }
421 | renderer.priority = renderer.priority + priorityDiff;
422 | }
423 | }
424 |
425 | if (this._layerDirty) {
426 | this._layerDirty = false;
427 | this._setLayer(this.layer, this.entity);
428 | }
429 |
430 | const time = this.direction * this.speed * deltaTime * 1000;
431 | this._frame += time / this._resource.timePerFrame;
432 | const clip = this._clip;
433 |
434 | if (this._spill()) {
435 | const { duration } = this._resource;
436 | this._resetElements();
437 |
438 | if (this.repeats > 0 || this.isLooping) {
439 | if (this.repeats > 0) {
440 | --this.repeats;
441 | }
442 |
443 | if (this.isAlternate) {
444 | this.direction *= -1;
445 | if (clip) {
446 | this._frame = Tools.codomainBounce(this._frame, 0, clip.end - clip.start);
447 | } else {
448 | this._frame = Tools.codomainBounce(this._frame, 0, duration);
449 | }
450 | } else {
451 | this.direction = 1;
452 | if (clip) {
453 | this._frame = Tools.euclideanModulo(this._frame, clip.end - clip.start);
454 | } else {
455 | this._frame = Tools.euclideanModulo(this._frame, duration);
456 | }
457 | }
458 | } else {
459 | if (clip) {
460 | if (this._frame >= clip.end - clip.start) {
461 | const endCallback = this._clipEndCallbacks[clip.name];
462 | if (endCallback) {
463 | endCallback(clip);
464 | }
465 | }
466 |
467 | this._frame = Tools.clamp(this._frame, 0, clip.end - clip.start);
468 | } else {
469 | if (this._frame >= duration) {
470 | const endCallback = this._clipEndCallbacks["ALL"];
471 | if (endCallback) {
472 | endCallback();
473 | }
474 | }
475 |
476 | this._frame = Tools.clamp(this._frame, 0, duration);
477 | }
478 | }
479 | }
480 |
481 | if (clip) {
482 | this._updateElements(this._resource.inPoint + this._frame + clip.start);
483 | } else {
484 | this._updateElements(this._resource.inPoint + this._frame);
485 | }
486 | }
487 |
488 | /**
489 | * is this time frame spill the range
490 | */
491 | private _spill(): boolean {
492 | let duration: number;
493 |
494 | if (this._clip) {
495 | const clip = this._clip;
496 | duration = clip.end - clip.start;
497 | } else {
498 | duration = this._resource.duration;
499 | }
500 |
501 | const bottomSpill = this._frame <= 0 && this.direction === -1;
502 | const topSpill = this._frame >= duration && this.direction === 1;
503 | return bottomSpill || topSpill;
504 | }
505 |
506 | /**
507 | * @override
508 | * @param target
509 | */
510 | _cloneTo(target) {
511 | target._createElements(this._resource, true);
512 | }
513 |
514 | private _destroy() {
515 | const elements = this._elements;
516 | if (elements) {
517 | for (let i = 0, l = elements.length; i < l; i++) {
518 | elements[i].destroy();
519 | }
520 | }
521 | }
522 |
523 | onDestroy(): void {
524 | this._destroy();
525 | }
526 | }
527 |
--------------------------------------------------------------------------------