Thanks,
Here the code:
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - animation - basic</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - animation - basic use
</div>
<div class="btnContainer">
<button class="btn" id="btnSaveAnim">Save moves</button>
</div>
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three@0.132.2";
import { GLTFExporter } from "https://cdn.skypack.dev/three@0.132.2/examples/jsm/exporters/GLTFExporter.js";
let clock;
let scene, camera, renderer, mixer, clip;
let isSavingAnim = false;
init();
animate();
const downloadAnim = (blob, fileName) => {
const url = URL.createObjectURL(blob);
console.log("url: ", url)
const a = document.createElement('a');
a.style = "display: none;";
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
setTimeout(() => {
URL.revokeObjectURL(url);
document.body.removeChild(a);
}, 0);
}
function saveString(text, filename) {
downloadAnim( new Blob( [text], { type: 'text/plain' } ), filename );
}
function saveArrayBuffer(buffer, filename) {
downloadAnim( new Blob( [buffer], { type: 'application/octet-stream' } ), filename );
}
const exportGLTF = (input) => {
const gltfExporter = new GLTFExporter();
const options = {
animations: [clip],
binary: true,
};
console.log(options);
gltfExporter.parse(
input,
function (result) {
if (result instanceof ArrayBuffer) {
saveArrayBuffer( result, 'scene.glb' );
} else {
const output = JSON.stringify( result, null, 2 );
console.log(JSON.parse(output));
saveString( output, 'scene.gltf' );
}
},
function (error) {
console.log( 'An error happened during parsing', error );
},
options
);
}
const saveAnimButton = document.querySelector('#btnSaveAnim');
saveAnimButton.addEventListener('click', () => {
if(!isSavingAnim) {
isSavingAnim = true;
saveAnimButton.textContent = 'Stop';
}
else {
isSavingAnim = false;
saveAnimButton.textContent = 'Save moves';
exportGLTF(scene);
}
})
function init() {
scene = new THREE.Scene();
//
camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.set( 25, 25, 50 );
camera.lookAt( scene.position );
//
const axesHelper = new THREE.AxesHelper( 10 );
scene.add( axesHelper );
//
const geometry = new THREE.BoxGeometry( 5, 5, 5 );
const material = new THREE.MeshBasicMaterial( { color: 0xffffff, transparent: true } );
const mesh = new THREE.Mesh( geometry, material );
mesh.name = "box";
scene.add( mesh );
// create a keyframe track (i.e. a timed sequence of keyframes) for each animated property
// Note: the keyframe track type should correspond to the type of the property being animated
// POSITION
const positionKF = new THREE.VectorKeyframeTrack( 'box.position', [ 0, 1, 2 ], [ 0, 0, 0, 30, 0, 0, 0, 0, 0 ] );
// SCALE
const scaleKF = new THREE.VectorKeyframeTrack( 'box.scale', [ 0, 1, 2 ], [ 1, 1, 1, 2, 2, 2, 1, 1, 1 ] );
// ROTATION
// Rotation should be performed using quaternions, using a THREE.QuaternionKeyframeTrack
// Interpolating Euler angles (.rotation property) can be problematic and is currently not supported
// set up rotation about x axis
const xAxis = new THREE.Vector3( 1, 0, 0 );
const qInitial = new THREE.Quaternion().setFromAxisAngle( xAxis, 0 );
const qFinal = new THREE.Quaternion().setFromAxisAngle( xAxis, Math.PI );
const quaternionKF = new THREE.QuaternionKeyframeTrack( 'box.quaternion', [ 0, 1, 2 ], [ qInitial.x, qInitial.y, qInitial.z, qInitial.w, qFinal.x, qFinal.y, qFinal.z, qFinal.w, qInitial.x, qInitial.y, qInitial.z, qInitial.w ] );
// COLOR
const colorKF = new THREE.ColorKeyframeTrack( 'box.material.color', [ 0, 1, 2 ], [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ], THREE.InterpolateDiscrete );
// OPACITY
const opacityKF = new THREE.NumberKeyframeTrack( 'box.material.opacity', [ 0, 1, 2 ], [ 1, 0, 1 ] );
// create an animation sequence with the tracks
// If a negative time value is passed, the duration will be calculated from the times of the passed tracks array
clip = new THREE.AnimationClip( 'Action', 3, [ scaleKF, positionKF, quaternionKF, colorKF, opacityKF ] );
// setup the THREE.AnimationMixer
mixer = new THREE.AnimationMixer( mesh );
// create a ClipAction and set it to play
const clipAction = mixer.clipAction( clip );
clipAction.play();
//
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
clock = new THREE.Clock();
//
window.addEventListener( 'resize', onWindowResize );
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
render();
}
function render() {
const delta = clock.getDelta();
if ( mixer ) {
mixer.update( delta );
}
renderer.render( scene, camera );
}
</script>
</body>
</html>