1. 4FIPS
  2. PHOTOS
  3. VIDEOS
  4. APPS
  5. CODE
  6. FORUMS
  7. ABOUT
// [WHALE BY FIPS @ 4FIPS.COM, (c) 2018 FILIP STOKLAS, MIT-LICENSED]

if (Detector.webgl) {
    render_loop(init());
}

function init() {
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0xffffff);
    scene.fog = new THREE.FogExp2(0xffffff, 0.09);

    const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 30);
    camera.position.set(.0, .0, 10);

    const light = new THREE.HemisphereLight(0xffffff, 0xeeeeff);
    light.position.set(0, 1, 0);
    scene.add(light);

    const canvas = document.getElementById("whale_canvas");
    renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.gammaOutput = true;

    const state = {
        clock: new THREE.Clock(),

        scene: scene,
        camera: camera,
        canvas: canvas,
        renderer: renderer,

        model_dir: new THREE.Vector3(0, 0, 1),
        target_pos: new THREE.Vector3(0, 3, 8),
        follow_dur: 0,

        model: null,
        mixer: null,
    }

    // load model asynchronously
    const loader = new THREE.GLTFLoader();
    loader.load('scene/scene.gltf', function (gltf) {
        state.model = gltf.scene;
        scene.add(state.model);
        state.model.position.set(0, -8, -3);
        state.mixer = new THREE.AnimationMixer(state.model);
        state.mixer.clipAction(gltf.animations[0]).play();
    });

    // install event handlers:

    document.addEventListener('mousedown', function(){
        change_target(state, (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1, -8 + 3 * Math.random());
    }, false);

    document.addEventListener('touchstart', function(){
        change_target(state, (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1, -8 + 3 * Math.random());
    }, false);

    return state;
}

function resize(state) {
    const width = state.canvas.clientWidth;
    const height = state.canvas.clientHeight;
    if (width != state.canvas.width || height != state.canvas.height) {
        state.renderer.setSize(width, height, false);
        state.camera.aspect = width / height;
        state.camera.updateProjectionMatrix();
    }
}

function render(state) {
    resize(state);

    const time = state.clock.elapsedTime;
    const time_delta = state.clock.getDelta();

    if (state.model && state.mixer) { // wait for the model

        if (state.follow_dur > 12) {
            change_target(state, -1.1 + 2.2 * Math.random(), -1.1 + 2.2 * Math.random(), -2 + 12 * Math.random());
        }
        state.follow_dur += time_delta;

        state.mixer.update(.9 * time_delta);

        // move towards the target
        {
            const speed = 0.015;
            const pos_delta = state.model_dir.clone().multiplyScalar(speed);
            state.model.position.add(pos_delta);
        }

        // orient towards the target
        {
            const inertia = 200 -  Math.min(50 * state.follow_dur, 150);
            const target_dir = state.target_pos.clone().sub(state.model.position).normalize();
            state.model_dir.multiplyScalar(inertia).add(target_dir).normalize();
            state.model.lookAt(state.model.position.clone().add(state.model_dir));
        }
    }
    
    state.renderer.render(state.scene, state.camera);
}

function render_loop(state) {
   resize(state);
   render(state);
   requestAnimationFrame(function(timestamp){ render_loop(state); });
}

function change_target(state, ndc_x, ndc_y, depth) { // normalized device coordinates <-1, 1>
    const vec = new THREE.Vector3(ndc_x, ndc_y, .5);
    const unpvec = vec.clone().unproject(state.camera);
    const dir = unpvec.clone().sub(state.camera.position).normalize();
    const distance = -state.camera.position.z / dir.z;
    const rand = dir.clone().multiplyScalar(depth);
    const pos = state.camera.position.clone().add(dir.clone().multiplyScalar(distance)).add(rand);
    state.target_pos.copy(pos);
    state.follow_dur = 0;
}