├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── src
├── adapter
│ ├── XMLHttpRequest.js
│ └── index.js
├── index.d.ts
└── index.js
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/index.js
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "babel-eslint",
4 | parserOptions: {
5 | ecmaVersion: 2018,
6 | sourceType: 'module'
7 | },
8 | env: {
9 | es6: true,
10 | node: true
11 | },
12 | extends: [
13 | "eslint:recommended"
14 | ],
15 | globals: {
16 | 'XMLHttpRequest': true,
17 | 'window': true,
18 | 'document': true,
19 | 'navigator': true,
20 | 'wx': true
21 | },
22 | rules: {
23 | 'no-console': process.env.NODE_ENV !== 'production' ? 0 : 2,
24 | 'no-useless-escape': 0,
25 | 'no-empty': 0
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | miniprogram_dist
3 | miniprogram_dev
4 | dist
5 | miniprogram_dist
6 |
7 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 wechat-miniprogram
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lottie for MiniProgram
2 |
3 | [](https://www.npmjs.com/package/lottie-miniprogram)
4 | [](https://github.com/wechat-miniprogram/lottie-miniprogram)
5 |
6 | lottie 动画库适配小程序的版本。
7 |
8 | > lottie 的相关介绍与动画生成方法等请参考[官方说明](https://github.com/airbnb/lottie-web)
9 |
10 | > 依赖小程序基础库版本 >= 2.8.0 的环境
11 |
12 | ## 使用
13 |
14 | 可参考该代码片段:[https://developers.weixin.qq.com/s/2TYvm9mJ75bF](https://developers.weixin.qq.com/s/2TYvm9mJ75bF)。大致步骤如下:
15 |
16 | 1. 通过 npm 安装:
17 | ```
18 | npm install --save lottie-miniprogram
19 | ```
20 |
21 | 2. 传入 canvas 对象用于适配
22 | ```
23 |
24 | ```
25 | ```
26 | import lottie from 'lottie-miniprogram'
27 |
28 | Page({
29 | onReady() {
30 | this.createSelectorQuery().select('#canvas').node(res => {
31 | const canvas = res.node
32 | lottie.setup(canvas)
33 | }).exec()
34 | }
35 | })
36 | ```
37 |
38 | 3. 使用 lottie 接口
39 | ```
40 | lottie.setup(canvas)
41 | this.ani = lottie.loadAnimation({
42 | ...
43 | })
44 | this.ani.destroy() // 页面退出需销毁
45 | ```
46 |
47 | ## 接口
48 |
49 | 目前提供两个接口:
50 |
51 | #### lottie.setup(canvas)
52 | 需要在任何 lottie 接口调用之前调用,传入 canvas 对象
53 |
54 | #### lottie.loadAnimation(options)
55 | 与原来的 [loadAnimation](https://github.com/airbnb/lottie-web/wiki/loadAnimation-options) 有些不同,支持的参数有:
56 | * loop
57 | * autoplay
58 | * animationData
59 | * path (只支持网络地址)
60 | * rendererSettings.context (必填)
61 |
62 | ## 说明
63 | * 本项目是以 npm 的方式依赖原 lottie-web 项目,若原项目有新版本,可直接改变依赖的版本号。
64 | * 本项目依赖小程序基础库 2.8.0 里性能更好的 canvas 实现,由于还有些小问题~~没有正式开放~~(2.9.0 已正式对外),但目前用在此处暂无发现问题。
65 | * 由于小程序本身不支持动态执行脚本,因此 lottie 的 expression 功能也是不支持的。
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lottie-miniprogram",
3 | "version": "1.0.12",
4 | "description": "lottie for miniprogram",
5 | "main": "miniprogram_dist/index.js",
6 | "scripts": {
7 | "dev": "webpack --mode=development",
8 | "build": "webpack --mode=production",
9 | "lint": "eslint \"src/**/*.js\""
10 | },
11 | "miniprogram": "miniprogram_dist",
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/wechat-miniprogram/lottie-miniprogram.git"
15 | },
16 | "keywords": [
17 | "lottie",
18 | "miniprogram"
19 | ],
20 | "author": "wechat-miniprogram",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/wechat-miniprogram/lottie-miniprogram/issues"
24 | },
25 | "homepage": "https://github.com/wechat-miniprogram/lottie-miniprogram#readme",
26 | "devDependencies": {
27 | "@babel/core": "^7.5.5",
28 | "@babel/plugin-proposal-class-properties": "^7.5.5",
29 | "@babel/preset-env": "^7.5.5",
30 | "babel-eslint": "^10.0.2",
31 | "babel-loader": "^8.0.6",
32 | "eslint": "^6.1.0",
33 | "eslint-loader": "^2.2.1",
34 | "string-replace-loader": "^2.2.0",
35 | "webpack": "^4.39.1",
36 | "webpack-cli": "^3.3.6",
37 | "lottie-web": "^5.7.4",
38 | "copy-webpack-plugin": "^6.0.3"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/adapter/XMLHttpRequest.js:
--------------------------------------------------------------------------------
1 | const _url = new WeakMap()
2 | const _method = new WeakMap()
3 | const _requestHeader = new WeakMap()
4 | const _responseHeader = new WeakMap()
5 | const _requestTask = new WeakMap()
6 |
7 | function _triggerEvent(type, ...args) {
8 | if (typeof this[`on${type}`] === 'function') {
9 | this[`on${type}`].apply(this, args)
10 | }
11 | }
12 |
13 | function _changeReadyState(readyState) {
14 | this.readyState = readyState
15 | _triggerEvent.call(this, 'readystatechange')
16 | }
17 |
18 | export default class XMLHttpRequest {
19 | // TODO 没法模拟 HEADERS_RECEIVED 和 LOADING 两个状态
20 | static UNSEND = 0
21 | static OPENED = 1
22 | static HEADERS_RECEIVED = 2
23 | static LOADING = 3
24 | static DONE = 4
25 |
26 | /*
27 | * TODO 这一批事件应该是在 XMLHttpRequestEventTarget.prototype 上面的
28 | */
29 | onabort = null
30 | onerror = null
31 | onload = null
32 | onloadstart = null
33 | onprogress = null
34 | ontimeout = null
35 | onloadend = null
36 |
37 | onreadystatechange = null
38 | readyState = 0
39 | response = null
40 | responseText = null
41 | responseType = ''
42 | responseXML = null
43 | status = 0
44 | statusText = ''
45 | upload = {}
46 | withCredentials = false
47 |
48 | constructor() {
49 | _requestHeader.set(this, {
50 | 'content-type': 'application/x-www-form-urlencoded'
51 | })
52 | _responseHeader.set(this, {})
53 | }
54 |
55 | abort() {
56 | const myRequestTask = _requestTask.get(this)
57 |
58 | if (myRequestTask) {
59 | myRequestTask.abort()
60 | }
61 | }
62 |
63 | getAllResponseHeaders() {
64 | const responseHeader = _responseHeader.get(this)
65 |
66 | return Object.keys(responseHeader).map((header) => {
67 | return `${header}: ${responseHeader[header]}`
68 | }).join('\n')
69 | }
70 |
71 | getResponseHeader(header) {
72 | return _responseHeader.get(this)[header]
73 | }
74 |
75 | open(method, url/* async, user, password 这几个参数在小程序内不支持*/) {
76 | _method.set(this, method)
77 | _url.set(this, url)
78 | _changeReadyState.call(this, XMLHttpRequest.OPENED)
79 | }
80 |
81 | overrideMimeType() {
82 | }
83 |
84 | send(data = '') {
85 | if (this.readyState !== XMLHttpRequest.OPENED) {
86 | throw new Error("Failed to execute 'send' on 'XMLHttpRequest': The object's state must be OPENED.")
87 | } else {
88 | wx.request({
89 | data,
90 | url: _url.get(this),
91 | method: _method.get(this),
92 | header: _requestHeader.get(this),
93 | // responseType: this.responseType,
94 | success: ({ data, statusCode, header }) => {
95 | if (typeof data !== 'string' && !(data instanceof ArrayBuffer)) {
96 | try {
97 | data = JSON.stringify(data)
98 | } catch (e) {
99 | }
100 | }
101 |
102 | this.status = statusCode
103 | _responseHeader.set(this, header)
104 | _triggerEvent.call(this, 'loadstart')
105 | _changeReadyState.call(this, XMLHttpRequest.HEADERS_RECEIVED)
106 | _changeReadyState.call(this, XMLHttpRequest.LOADING)
107 |
108 | this.response = data
109 |
110 | if (data instanceof ArrayBuffer) {
111 | this.responseText = ''
112 | const bytes = new Uint8Array(data)
113 | const len = bytes.byteLength
114 |
115 | for (let i = 0; i < len; i++) {
116 | this.responseText += String.fromCharCode(bytes[i])
117 | }
118 | } else {
119 | this.responseText = data
120 | }
121 | _changeReadyState.call(this, XMLHttpRequest.DONE)
122 | _triggerEvent.call(this, 'load')
123 | _triggerEvent.call(this, 'loadend')
124 | },
125 | fail: ({ errMsg }) => {
126 | // TODO 规范错误
127 | if (errMsg.indexOf('abort') !== -1) {
128 | _triggerEvent.call(this, 'abort')
129 | } else {
130 | _triggerEvent.call(this, 'error', errMsg)
131 | }
132 | _triggerEvent.call(this, 'loadend')
133 | }
134 | })
135 | }
136 | }
137 |
138 | setRequestHeader(header, value) {
139 | const myHeader = _requestHeader.get(this)
140 |
141 | myHeader[header] = value
142 | _requestHeader.set(this, myHeader)
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/adapter/index.js:
--------------------------------------------------------------------------------
1 | import XHR from './XMLHttpRequest'
2 |
3 | function noop() {}
4 |
5 | function notSupport() {
6 | console.error('小程序由于不支持动态创建 canvas 的能力,故 lottie 中有关图片处理的操作无法支持,请保持图片的原始宽高与 JSON 描述的一致,避免需要对图片处理')
7 | }
8 |
9 | function createImg(canvas) {
10 | if (typeof canvas.createImage === 'undefined') {
11 | // TODO the return value should be replaced after setupLottie
12 | return {}
13 | }
14 | const img = canvas.createImage()
15 | img.addEventListener = img.addEventListener || function (evtName, callback) {
16 | if (evtName === 'load') {
17 | img.onload = function () {
18 | setTimeout(callback, 0)
19 | }
20 | } else if (evtName === 'error') {
21 | img.onerror = callback
22 | }
23 | }
24 | return img
25 | }
26 |
27 | function createElement(tagName) {
28 | if (tagName === 'canvas') {
29 | console.warn('发现 Lottie 动态创建 canvas 组件,但小程序不支持动态创建组件,接下来可能会出现异常')
30 | return {
31 | getContext: function () {
32 | return {
33 | fillRect: noop,
34 | createImage: notSupport,
35 | drawImage: notSupport,
36 | }
37 | },
38 | }
39 | } else if (tagName === 'img') {
40 | return createImg(this)
41 | }
42 | }
43 |
44 | function wrapSetLineDash(ctx, originalSetLineDash) {
45 | return function setLineDash(segments) {
46 | return originalSetLineDash.call(ctx, Array.from(segments))
47 | }
48 | }
49 |
50 | function wrapFill(ctx, originalFill) {
51 | return function fill() {
52 | // ignore parameters which causes iOS wechat 7.0.5 crash.
53 | return originalFill.call(ctx)
54 | }
55 | }
56 |
57 | function wrapMethodFatory(ctx, methodName, wrappedMethod) {
58 | const originalMethod = ctx[methodName]
59 | ctx[methodName] = wrappedMethod(ctx, originalMethod)
60 | }
61 |
62 | const systemInfo = wx.getSystemInfoSync()
63 | const g = {
64 | requestAnimationFrame(cb) {
65 | setTimeout(() => {
66 | typeof cb === 'function' && cb(Date.now())
67 | }, 16)
68 | },
69 | }
70 |
71 | g.window = {
72 | devicePixelRatio: systemInfo.pixelRatio,
73 | }
74 | g.document = g.window.document = {
75 | body: {},
76 | createElement,
77 | }
78 | g.navigator = g.window.navigator = {
79 | userAgent: ''
80 | }
81 |
82 | XMLHttpRequest = XHR
83 |
84 | export const setup = (canvas) => {
85 | const {window, document} = g
86 | g._requestAnimationFrame = window.requestAnimationFrame
87 | g._cancelAnimationFrame = window.cancelAnimationFrame
88 | // lottie 对象是单例,内部状态(_stopped)在多页面下会混乱,保持 rAF 持续运行可规避
89 | window.requestAnimationFrame = function requestAnimationFrame(cb) {
90 | let called = false
91 | setTimeout(() => {
92 | if (called) return
93 | called = true
94 | typeof cb === 'function' && cb(Date.now())
95 | }, 100)
96 | canvas.requestAnimationFrame((timeStamp) => {
97 | if (called) return
98 | called = true
99 | typeof cb === 'function' && cb(timeStamp)
100 | })
101 | }
102 | window.cancelAnimationFrame = canvas.cancelAnimationFrame.bind(canvas)
103 |
104 | g._body = document.body
105 | g._createElement = document.createElement
106 | document.body = {}
107 | document.createElement = createElement.bind(canvas)
108 |
109 | const ctx = canvas.getContext('2d')
110 | if (!ctx.canvas) {
111 | ctx.canvas = canvas
112 | }
113 |
114 | wrapMethodFatory(ctx, 'setLineDash', wrapSetLineDash)
115 | wrapMethodFatory(ctx, 'fill', wrapFill)
116 | }
117 |
118 | export const restore = () => {
119 | const {window, document} = g
120 | window.requestAnimationFrame = g._requestAnimationFrame
121 | window.cancelAnimationFrame = g._cancelAnimationFrame
122 | document.body = g._body
123 | document.createElement = g._createElement
124 | }
125 |
126 | export {g}
127 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | type BaseRendererConfig = {
2 | imagePreserveAspectRatio?: string;
3 | className?: string;
4 | };
5 |
6 | type CanvasRendererConfig = BaseRendererConfig & {
7 | clearCanvas?: boolean;
8 | context: CanvasRenderingContext2D;
9 | progressiveLoad?: boolean;
10 | preserveAspectRatio?: string;
11 | };
12 |
13 | interface LoadAnimationParameter {
14 | renderer?: 'canvas';
15 | loop?: boolean | number;
16 | autoplay?: boolean;
17 | name?: string;
18 | rendererSettings?: CanvasRendererConfig;
19 | animationData?: any;
20 | path?: string;
21 | }
22 |
23 | type AnimationDirection = 1 | -1;
24 | type AnimationSegment = [number, number];
25 | type AnimationEventName = 'enterFrame' | 'loopComplete' | 'complete' | 'segmentStart' | 'destroy';
26 | type AnimationEventCallback = (args: T) => void;
27 |
28 | interface LoadAnimationReturnType {
29 | play(): void;
30 | stop(): void;
31 | pause(): void;
32 | setSpeed(speed: number): void;
33 | goToAndPlay(value: number, isFrame?: boolean): void;
34 | goToAndStop(value: number, isFrame?: boolean): void;
35 | setDirection(direction: AnimationDirection): void;
36 | playSegments(segments: AnimationSegment | AnimationSegment[], forceFlag?: boolean): void;
37 | setSubframe(useSubFrames: boolean): void;
38 | destroy(): void;
39 | getDuration(inFrames?: boolean): number;
40 | triggerEvent(name: AnimationEventName, args: T): void;
41 | addEventListener(name: AnimationEventName, callback: AnimationEventCallback): void;
42 | removeEventListener(name: AnimationEventName, callback: AnimationEventCallback): void;
43 | }
44 |
45 | declare module lottie {
46 | var loadAnimation: (options: LoadAnimationParameter) => LoadAnimationReturnType;
47 | var setup: (node: any) => void;
48 | }
49 |
50 | export default lottie;
51 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {setup, g, restore} from './adapter'
2 | const {window, document, navigator} = g
3 |
4 | ;'__LOTTIE_CANVAS__'
5 |
6 | function loadAnimation(options) {
7 | ['wrapper', 'container'].forEach(key => {
8 | if (key in options) {
9 | throw new Error(`Not support '${key}' parameter in miniprogram version of lottie.`)
10 | }
11 | })
12 | if (typeof options.path === 'string' && !/^https?\:\/\//.test(options.path)) {
13 | throw new Error(`The 'path' is only support http protocol.`)
14 | }
15 | if (!options.rendererSettings || !options.rendererSettings.context) {
16 | throw new Error(`Parameter 'rendererSettings.context' should be a CanvasRenderingContext2D.`)
17 | }
18 | options.renderer = 'canvas'
19 |
20 | const _aniItem = window.lottie.loadAnimation(options)
21 | // try to fix https://github.com/airbnb/lottie-web/issues/1772
22 | const originalDestroy = _aniItem.destroy.bind(_aniItem)
23 | _aniItem.destroy = function () {
24 | // 恢复到上一次 canvas 的环境,避免当前 canvas 被销毁后导致 lottie-web 内部死锁
25 | restore()
26 | if (_aniItem.renderer && !_aniItem.renderer.destroyed) {
27 | _aniItem.renderer.renderConfig.clearCanvas = false
28 | }
29 | originalDestroy()
30 | }.bind(_aniItem)
31 |
32 | return _aniItem
33 | }
34 |
35 | const {freeze, unfreeze} = window.lottie
36 |
37 | export {
38 | setup,
39 | loadAnimation,
40 | freeze,
41 | unfreeze,
42 | }
43 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const fs = require('fs')
4 | const CopyPlugin = require('copy-webpack-plugin')
5 |
6 | module.exports = {
7 | entry: './src/index.js',
8 | output: {
9 | libraryTarget: 'commonjs',
10 | filename: 'index.js',
11 | path: path.resolve(__dirname, 'miniprogram_dist'),
12 | },
13 | devtool: '',
14 | module: {
15 | rules: [{
16 | test: /\.js$/i,
17 | use: [{
18 | loader: 'eslint-loader',
19 | }, {
20 | loader: 'babel-loader',
21 | options: {
22 | presets: ['@babel/preset-env'],
23 | plugins: ['@babel/plugin-proposal-class-properties'],
24 | },
25 | }, {
26 | loader: 'string-replace-loader',
27 | options: {
28 | multiple: [{
29 | search: `'__LOTTIE_CANVAS__'`,
30 | replace: fs.readFileSync('./node_modules/lottie-web/build/player/lottie_canvas.js', {encoding: 'utf8'}),
31 | }, {
32 | search: '__[STANDALONE]__',
33 | replace: '',
34 | }]
35 | }
36 | }],
37 | exclude: /node_modules/
38 | }]
39 | },
40 | amd: false,
41 | plugins: [
42 | new webpack.DefinePlugin({
43 | 'define': {}
44 | }),
45 | new webpack.optimize.ModuleConcatenationPlugin(),
46 | new CopyPlugin({
47 | patterns: [
48 | {
49 | from: path.resolve(__dirname, 'src', 'index.d.ts'),
50 | to: path.resolve(__dirname, 'miniprogram_dist', 'index.d.ts')
51 | },
52 | ],
53 | }),
54 | ],
55 | }
56 |
--------------------------------------------------------------------------------