# 动画

使用 [Tween](https://vjmap.com/map3d/api/classes/Tween.html) 主要用于创建平滑的补间动画效果。它允许开发者通过简单地设置起始和结束状态，以及动画的持续时间，来轻松地创建数值、颜色、甚至对象属性随时间变化的动画。`Tween` 提供了丰富的缓动（easing）功能，使得动画不仅能够按预期的时间进行，还能具备不同的速度变化模式，比如加速、减速或者弹跳效果，这让动画看起来更加自然和吸引人。

## 用法

```js
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshPhongMaterial({color: '#f00'});
const mesh = new THREE.Mesh(geometry, mat);
let boxEnt = new vjmap3d.MeshEntity(mesh);
boxEnt.addTo(app);
// 创建补间动画一
boxEnt.tween().by(1000, { position: new THREE.Vector3(0, 0.5, 0) }, { easing: 'easeInOutBack' }).yoyoForever().start();
// 创建补间动画二
new vjmap3d.Tween(boxEnt)
  .by(2000, { scale: 1, rotation: new THREE.Euler(Math.PI * 2, Math.PI, 0) }, { easing: 'easeOutElastic' })
  .delay(200)
  .to(1000, { scale: 1 }, { easing: 'easeOutBounce' })
  .start();
```

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true }, // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshPhongMaterial({color: '#f00'});
const mesh = new THREE.Mesh(geometry, mat);
let boxEnt = new vjmap3d.MeshEntity(mesh);
boxEnt.addTo(app);
// 创建补间动画一
boxEnt.tween().by(1000, { position: new THREE.Vector3(0, 0.5, 0) }, { easing: 'easeInOutBack' }).yoyoForever().start();
// 创建补间动画二
new vjmap3d.Tween(boxEnt)
  .by(2000, { scale: 1, rotation: new THREE.Euler(Math.PI * 2, Math.PI, 0) }, { easing: 'easeOutElastic' })
  .delay(200)
  .to(1000, { scale: 1 }, { easing: 'easeOutBounce' })
  .start();
</pre>
</mapFrame>


## 常用方法

Tween类常用方法

|方法|描述|
|:---|:---|
|[setId](https://vjmap.com/map3d/api/classes/Tween.html#setId)|为动画设置一个唯一的标识符。如果指定，则停止具有相同id的旧动画|
|[setTags](https://vjmap.com/map3d/api/classes/Tween.html#setTags)|为动画设置标签，可用于过滤和管理|
|[setTarget](https://vjmap.com/map3d/api/classes/Tween.html#setTarget)|设置动画的目标对象|
|[to](https://vjmap.com/map3d/api/classes/Tween.html#to)|`to(毫秒, {属性: 目标值}, config?)`：从执行到该步时的当前值补间到**绝对**目标值（相对值请用 `by`）。未指定 `easing` 时默认 `easeInOutExpo`|
|[by](https://vjmap.com/map3d/api/classes/Tween.html#by)|`by(毫秒, {属性: 增量}, config?)`：在当前值基础上做**相对**运动（目标 = 当前值 + 增量）|
|[set](https://vjmap.com/map3d/api/classes/Tween.html#set)|立即定义从当前状态到新状态的移动。|
|[call](https://vjmap.com/map3d/api/classes/Tween.html#call)|增加回调函数|
|[delay](https://vjmap.com/map3d/api/classes/Tween.html#delay)|`delay(毫秒)`：在动作序列的**当前位置**插入一段等待——写在第一个 `to/by` 之前即延迟开始，写在两个动作之间即中途停顿|
|[repeat](https://vjmap.com/map3d/api/classes/Tween.html#repeat)|`repeat(n)`：把**紧挨着的前一个**动作（一个 `to/by/delay/call`，或一个 `parallel/then/sequence` 子动画）再重复 n 次，即共执行 **n+1** 次；不会重复整条链——要整体重复，先把多个动作包进一个子 Tween 再 `then(...)`/`parallel(...)`|
|[repeatForever](https://vjmap.com/map3d/api/classes/Tween.html#repeatForever)|无限期地重复上一个操作（同样只针对紧挨着的前一个动作）|
|[yoyo](https://vjmap.com/map3d/api/classes/Tween.html#yoyo)|`yoyo(n)`：把**紧挨着的前一个**动作反向/正向交替再播放 n 次，像悠悠球一样来回运动，而不是重新开始|
|[yoyoForever](https://vjmap.com/map3d/api/classes/Tween.html#yoyoForever)|对最后一个动作应用溜溜球效果，使其无限期地反转运动|
|[parallel](https://vjmap.com/map3d/api/classes/Tween.html#parallel)|并行运行多条。⚠️ 子 Tween 自己的 target 被忽略，一律作用在**父 Tween 的 target** 上|
|[sequence](https://vjmap.com/map3d/api/classes/Tween.html#sequence)|按顺序运行多条（同样作用在父 Tween 的 target 上）|
|[chain](https://vjmap.com/map3d/api/classes/Tween.html#chain)|链式补间：把另一个 Tween 的动作**追加**到本 Tween 末尾、顺序执行（复制的是动作，对方的 target 被忽略，作用在本 Tween 的 target 上）|
|[clone](https://vjmap.com/map3d/api/classes/Tween.html#clone)|克隆动画|
|[start](https://vjmap.com/map3d/api/classes/Tween.html#start)|补间动画启动的方法，返回 `RunningTween`（见下文"动画控制"）|

### 动画控制

```js
let running = new vjmap3d.Tween(boxEnt).to(2000, { position: new THREE.Vector3(5, 0, 0) }).start();
running.pause();          // 暂停
running.resume();         // 继续
running.setTimeScale(2);  // 2 倍速
running.complete();       // 立即跳到结束
running.stop();           // 立即停止（会触发一次 onComplete(target, true, true)）
// 通过 app.tweenManager 批量管理
app.tweenManager.stopById("myId");    // 对应 tween.setId("myId")
app.tweenManager.stopAllByTag("tag1"); // 对应 tween.setTags("tag1")
app.tweenManager.stopAll();
```

> - **缓动默认值不同**：`to/by` 未指定 `easing` 时默认 `easeInOutExpo`；`vjmap3d.animation()`/`animationAsync()` 默认 `linear`。
> - ⚠️ **`easing: 'easeInBounce'` / `'easeInOutBounce'` 不能用字符串**：这两个函数内部用 `this.easeOutBounce`，按字符串取出后 `this` 丢失，每帧抛 `this.easeOutBounce is not a function`，渲染随之中断。改传函数：`easing: x => vjmap3d.Easings.easeInOutBounce(x)`。
> - **时间单位**：Tween/`animation` 的时长是**毫秒**；而行为组件（[行为组件](./behavior.md)）`tick(timeDelta)` 收到的是**秒**。

## 动画事件

回调写在每个 `to/by` 的 `config` 里，**按动作块分别触发**（一个 Tween 里有多个 `to/by` 就各自触发各自的）：
- `onStart(target)`：该动作块开始时；`onUpdate(target)`：每帧写完属性后；
- `onComplete(target, isEnd, isStop)`：该动作块结束时，`isEnd` 表示整个 Tween 是否已全部结束；调用 `RunningTween.stop()` 也会触发一次（`isEnd`、`isStop` 均为 `true`）；
- `onProgress(target, key, start, end, alpha, reversed)`：每个属性写入前调用，返回 `false` 则跳过本次写入（可自己接管赋值）。

```js
let material = boxEnt.node.material // boxEnt 为下例创建的 MeshEntity
new vjmap3d.Tween(material).to(500, { color: new THREE.Color('#0f0') }, { easing: 'easeOutElastic',
    onStart: (target) => {
        console.log("onStart" , target)
    },
    onComplete: (target, isEnd, isStop) => {
        console.log("onComplete" , target, isEnd, isStop)
    },
    onUpdate: (target) => {
        console.log("onUpdate" , target)
    },
    onProgress: (targe, key, start, end, alpha, reversed) => {
        console.log("onProgress" , targe, key, start, end, alpha, reversed)
    }
}).start();
```


```js
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshPhongMaterial({color: '#f00'});
const mesh = new THREE.Mesh(geometry, mat);
let boxEnt = new vjmap3d.MeshEntity(mesh);
boxEnt.addTo(app);
boxEnt.pointerEvents = true;
boxEnt.on("mouseover", e => {
    boxEnt.selected = true;
    app.setCursor("pointer");
    // id必须要指定并且下mouseout的id一样。要不会造成两个不同的动画。不会取消之前的了
    boxEnt.tween('id').to(500, { scale: new THREE.Vector3(1.5, 1.5, 1.5) }, { easing: 'easeOutElastic' }).start();
})
boxEnt.on("mouseout", e => {
    boxEnt.selected = false;
    app.setCursor("");
    boxEnt.tween('id').to(500, { scale: new THREE.Vector3(1, 1, 1) }, { easing: 'easeOutElastic' }).start();
})
```

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true }, // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshPhongMaterial({color: '#f00'});
const mesh = new THREE.Mesh(geometry, mat);
let boxEnt = new vjmap3d.MeshEntity(mesh);
boxEnt.addTo(app);
boxEnt.pointerEvents = true;
boxEnt.on("mouseover", e => {
    boxEnt.selected = true;
    app.setCursor("pointer");
    // id必须要指定并且下mouseout的id一样。要不会造成两个不同的动画。不会取消之前的了
    boxEnt.tween('id').to(500, { scale: new THREE.Vector3(1.5, 1.5, 1.5) }, { easing: 'easeOutElastic' }).start();
})
boxEnt.on("mouseout", e => {
    boxEnt.selected = false;
    app.setCursor("");
    boxEnt.tween('id').to(500, { scale: new THREE.Vector3(1, 1, 1) }, { easing: 'easeOutElastic' }).start();
})
</pre>
</mapFrame>





## 同时多个动画
```js
let tween1 = new vjmap3d.Tween(boxEnt).by(2000, { position: new THREE.Vector3(-5, 0, 3) });
let tween2 = new vjmap3d.Tween(boxEnt).by(3000, { scale: new THREE.Vector3(1.5, 1.5, 1.5) });
tween1.chain(tween2);
tween1.start()
```

```js
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshPhongMaterial({color: '#f00'});
const mesh = new THREE.Mesh(geometry, mat);
let boxEnt = new vjmap3d.MeshEntity(mesh);
boxEnt.addTo(app);
let tween1 = new vjmap3d.Tween(boxEnt).by(2000, { position: new THREE.Vector3(-5, 0, 3) });
let tween2 = new vjmap3d.Tween(boxEnt).by(3000, { scale: new THREE.Vector3(1.5, 1.5, 1.5) });
let tween = new vjmap3d.Tween(boxEnt);
// 并行多个动画
tween.parallel(tween1, tween2).yoyoForever().start()
//
const geometry2 = new THREE.BoxGeometry(1, 1, 1);
const mat2 = new THREE.MeshPhongMaterial({color: '#0ff'});
const mesh2 = new THREE.Mesh(geometry2, mat2);
let boxEnt2 = new vjmap3d.MeshEntity(mesh2);
boxEnt2.addTo(app);
boxEnt2.position.set(0, 0, -2)
let tween4 = new vjmap3d.Tween(boxEnt2).by(2000, { position: new THREE.Vector3(-5, 0, -3) });
let tween5 = new vjmap3d.Tween(boxEnt2).by(3000, { scale: new THREE.Vector3(1.5, 1.5, 1.5) });
let tween3 = new vjmap3d.Tween(boxEnt2);
// 顺序执行多个动画
tween3.sequence(tween4, tween5).start()
```


<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true }, // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshPhongMaterial({color: '#f00'});
const mesh = new THREE.Mesh(geometry, mat);
let boxEnt = new vjmap3d.MeshEntity(mesh);
boxEnt.addTo(app);
let tween1 = new vjmap3d.Tween(boxEnt).by(2000, { position: new THREE.Vector3(-5, 0, 3) });
let tween2 = new vjmap3d.Tween(boxEnt).by(3000, { scale: new THREE.Vector3(1.5, 1.5, 1.5) });
let tween = new vjmap3d.Tween(boxEnt);
tween.parallel(tween1, tween2).yoyoForever().start()
//
const geometry2 = new THREE.BoxGeometry(1, 1, 1);
const mat2 = new THREE.MeshPhongMaterial({color: '#0ff'});
const mesh2 = new THREE.Mesh(geometry2, mat2);
let boxEnt2 = new vjmap3d.MeshEntity(mesh2);
boxEnt2.addTo(app);
boxEnt2.position.set(0, 0, -2)
let tween4 = new vjmap3d.Tween(boxEnt2).by(2000, { position: new THREE.Vector3(-5, 0, -3) });
let tween5 = new vjmap3d.Tween(boxEnt2).by(3000, { scale: new THREE.Vector3(1.5, 1.5, 1.5) });
let tween3 = new vjmap3d.Tween(boxEnt2);
tween3.sequence(tween4, tween5).start()
</pre>
</mapFrame>

## 动画封装函数 
[animation](https://vjmap.com/map3d/api/functions/animation.html) 创建动画，对Tween类的封装

[animationAsync](https://vjmap.com/map3d/api/functions/animationAsync.html) 创建动画，对Tween的封装，返回一个promise,动画结束后返回 `{ anim, target, isEnd, isStop }`

> ⚠️ `animationAsync` 只在整个动画结束（`isEnd` 为 true）时 resolve：`yoyoForever`/`repeatForever` 永远不会自然结束，`await` 会一直挂起，除非用 `app.tweenManager.stopById(id)`（需传 `id`）等方式停止。`repeatTimes: N` / `yoyoTimes: N` 是在首次播放之外**再**重复 N 次（共 N+1 遍），下例 `repeatTimes: 30` 约 93 秒后才 resolve。

```js
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshPhongMaterial({color: '#f00'});
const mesh = new THREE.Mesh(geometry, mat);
let boxEnt = new vjmap3d.MeshEntity(mesh);
boxEnt.addTo(app);
let color1 = new THREE.Color('#f00')
let color2 = new THREE.Color('#0f0')
vjmap3d.animation({
    duration: 3000,
    easing: "linear",
    yoyoForever: true,
    onProgress(target, key, start, end, alpha, reversed) {
        boxEnt.node.material.color = new THREE.Color().lerpColors(color1, color2, alpha)
    }
})
await vjmap3d.animationAsync({
    duration: 3000,
    easing: "linear",
    repeatTimes: 30,
    onProgress(target, key, start, end, alpha, reversed) {
        boxEnt.node.scale.set(alpha + 1, alpha + 1, alpha + 1)
    }
})
app.logInfo("缩放动画结束了", "success")
```

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true }, // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshPhongMaterial({color: '#f00'});
const mesh = new THREE.Mesh(geometry, mat);
let boxEnt = new vjmap3d.MeshEntity(mesh);
boxEnt.addTo(app);
let color1 = new THREE.Color('#f00')
let color2 = new THREE.Color('#0f0')
vjmap3d.animation({
    duration: 3000,
    easing: "linear",
    yoyoForever: true,
    onProgress(target, key, start, end, alpha, reversed) {
        boxEnt.node.material.color = new THREE.Color().lerpColors(color1, color2, alpha)
    }
})
await vjmap3d.animationAsync({
    duration: 3000,
    easing: "linear",
    repeatTimes: 30,
    onProgress(target, key, start, end, alpha, reversed) {
        boxEnt.node.scale.set(alpha + 1, alpha + 1, alpha + 1)
    }
})
app.logInfo("缩放动画结束了", "success")
</pre>
</mapFrame>


## 动画缓动函数

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    },
    control: { leftButtonPan: true } // 设置为左键用于平移 (同时右键将用于旋转) 和地图2d使用习惯一样
})
let image = await vjmap3d.loadImage(env.assetsPath + "image/imgbg2.png");
let easingCurve = vjmap3d.render2d.Path.extend({
    type: "easingCurve",
    shape: {
        x: 0,
        y: 0,
        width: 0,
        height: 0
    },
    buildPath(ctx, shape) {
        let x = shape.x;
        let y = shape.y;
        let height = shape.height;
        let width = shape.width;
        ctx.moveTo(x, y + height);
        ctx.lineTo(x + width, y + height);
        ctx.moveTo(x, y);
        ctx.lineTo(x, y + height);
        ctx.moveTo(x, y);
        for(let n = 0; n < height; n++) {
            let py = y + height - vjmap3d.Easings[this.easing](n / height) * height
            let px = x + n;
            ctx.lineTo(px, py);
        }
    }
});
const createEasingCurve = (method, position) => {
    let curve = new easingCurve({
        shape: {
            x: 60,
            y: 60,
            width: 140,
            height: 100
        },
        style: {
            stroke: "#0ff"
        }
    })
    curve.easing = method;
    let spriteMarker = new vjmap3d.SpriteMarkerEntity(
    {
        position: position, // 位置 
        sizeAttenuation: false, // 固定像素大小，不随相机变化
        anchorX: "center",  // x对齐方式
        anchorY: "bottom", // y对齐方式
        allowOverlap: true,
        renderTexture: {
            autoCanvasSize: false,
            canvasWidth: 270, // 像素宽，这里是根据底图的像素宽高定的
            canvasHeight: 250, // 像素高，这里是根据底图的像素宽高定的
            sharedCanvas: false,
            elements: [
                new vjmap3d.render2d.Image({
                    style: {
                        x: 0,
                        y: 0,
                        image: image,
                        width: 270, // 像素宽，这里是图片像素宽高定的
                        height: 250 // 像素高，这里是图片像素宽高定的
                    }
                }),
                new vjmap3d.render2d.Text({
                    style: {
                        x: 138,
                        y: 20,
                        text: method,
                        fill: "#0ff",
                        font: '30px Arial',
                        verticalAlign: "top",
                        align: "center",
                    }
                }),
                curve
            ]
        }
    });
    spriteMarker.addTo(app)
}
const easingsMethods = Object.getOwnPropertyNames(vjmap3d.Easings)
.filter(prop => typeof vjmap3d.Easings[prop] === 'function');
// 绘制
for(let i = 0; i < easingsMethods.length; i++) {
    let len = 30;
    let pos = [i * len/ easingsMethods.length - len / 2, 0, 0]
    createEasingCurve(easingsMethods[i], pos);
}
app.cameraControl.loadState({
    cameraTarget: new THREE.Vector3(-14.64345295429849,-1.108395354581628e-17, -1.4500183140945189),
	cameraPosition: new THREE.Vector3(-14.643168396143746,0.6458405918854697, 1.0255224852778935)
})
</pre>
</mapFrame>