唯杰地图 logo唯杰地图
VJMAP
EN

动画

vjmap3d/references/advance/animation.md 18.5 KB430 行
原文

使用 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)
})