动画
使用 Tween 主要用于创建平滑的补间动画效果。它允许开发者通过简单地设置起始和结束状态,以及动画的持续时间,来轻松地创建数值、颜色、甚至对象属性随时间变化的动画。Tween 提供了丰富的缓动(easing)功能,使得动画不仅能够按预期的时间进行,还能具备不同的速度变化模式,比如加速、减速或者弹跳效果,这让动画看起来更加自然和吸引人。
用法
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();
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();
常用方法
Tween类常用方法
| 方法 | 描述 |
|---|---|
| setId | 为动画设置一个唯一的标识符。如果指定,则停止具有相同id的旧动画 |
| setTags | 为动画设置标签,可用于过滤和管理 |
| setTarget | 设置动画的目标对象 |
| to | to(毫秒, {属性: 目标值}, config?):从执行到该步时的当前值补间到绝对目标值(相对值请用 by)。未指定 easing 时默认 easeInOutExpo |
| by | by(毫秒, {属性: 增量}, config?):在当前值基础上做相对运动(目标 = 当前值 + 增量) |
| set | 立即定义从当前状态到新状态的移动。 |
| call | 增加回调函数 |
| delay | delay(毫秒):在动作序列的当前位置插入一段等待——写在第一个 to/by 之前即延迟开始,写在两个动作之间即中途停顿 |
| repeat | repeat(n):把紧挨着的前一个动作(一个 to/by/delay/call,或一个 parallel/then/sequence 子动画)再重复 n 次,即共执行 n+1 次;不会重复整条链——要整体重复,先把多个动作包进一个子 Tween 再 then(...)/parallel(...) |
| repeatForever | 无限期地重复上一个操作(同样只针对紧挨着的前一个动作) |
| yoyo | yoyo(n):把紧挨着的前一个动作反向/正向交替再播放 n 次,像悠悠球一样来回运动,而不是重新开始 |
| yoyoForever | 对最后一个动作应用溜溜球效果,使其无限期地反转运动 |
| parallel | 并行运行多条。⚠️ 子 Tween 自己的 target 被忽略,一律作用在父 Tween 的 target 上 |
| sequence | 按顺序运行多条(同样作用在父 Tween 的 target 上) |
| chain | 链式补间:把另一个 Tween 的动作追加到本 Tween 末尾、顺序执行(复制的是动作,对方的 target 被忽略,作用在本 Tween 的 target 上) |
| clone | 克隆动画 |
| start | 补间动画启动的方法,返回 RunningTween(见下文"动画控制") |
动画控制
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的时长是毫秒;而行为组件(行为组件)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则跳过本次写入(可自己接管赋值)。
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();
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();
})
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();
})
同时多个动画
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()
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()
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()
动画封装函数
animation 创建动画,对Tween类的封装
animationAsync 创建动画,对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。
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")
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")
动画缓动函数
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)
})