What’s on our mind?

Collection of articles, design, site, and resources made by designers and publisher @Menu View

index16
class ThreeText {
    constructor(
        options = {
            appContainerSelector: "[data-app-container]"
        }
    ) {
        this.options = options;
        this.appContainer = document.querySelector(
            this.options.appContainerSelector
        );
        this.iter = 0;

        // THREE items
        this.renderer;
        this.camera;
        this.scene;
        this.controls;
        this.imageTexture;
        this.uniforms;
        this.geometry;
        this.mesh;
        this.font;

        this.targetMeshScale = 1;

        // 마우스
        this.currentMouse = {
            x: 0,
            y: 0
        };
        this.targetMouse = {
            x: 0,
            y: 0
        };

        // 설정
        this.settings = {
            cameraDistance: 100,
            bgColor: 0x111,
            mouseEase: 0.05,
            blobMinScale: 1,
            blobMaxScale: 2,
            blobScaleEase: 0.05,
            blobInflate: 0.015,
            blobDeflate: 0.01
        };

        this.init();
    }

    init = async () => {
        await this.loadTexture();
        this.createUniforms();
        this.createApp();

        // Load font
        const fontLoader = new THREE.FontLoader();
        fontLoader.load(
            "https://assets.codepen.io/66496/noe-display-medium.json",
            (loadedFont) => {
                this.font = loadedFont;

                this.createItems();
                this.addEventListeners();
                this.update();
            }
        );
    };

    loadTexture = async () => {
        this.imageTexture = await new THREE.TextureLoader().load(
            "https://assets.codepen.io/66496/temple-ranch-hero.jpg"
        );
    };

    createUniforms = () => {
        this.uniforms = {
            imageTexture: {
                value: this.imageTexture
            }
        };
        this.updateUniforms();
    };

    updateUniforms = () => {
        Object.assign(
            this.uniforms, {}, {
                iter: {
                    value: this.iter
                },
                currentMouse: {
                    value: this.currentMouse
                }
            }
        );
    };

    createApp = () => {
        // 렌더링
        this.renderer = new THREE.WebGLRenderer({devicePixelRatio: 1.5, antialias: true});
        this.renderer.setSize(this.appContainer.offsetWidth,this.appContainer.offsetHeight);
        this.appContainer.appendChild(this.renderer.domElement);

        // 카메라 설정
        this.camera = new THREE.PerspectiveCamera(45, this.appContainer.offsetWidth / this.appContainer.offsetHeight, 1, 10000);
        this.camera.position.set(-3, 2, this.settings.cameraDistance);
        this.scene = new THREE.Scene();
        this.scene.background = new THREE.Color(this.settings.bgColor);

        // 움직임 컨트롤
        this.controls = new OrbitControls(this.camera, this.renderer.domElement);
        this.controls.enableKeys = false;
        this.controls.enableZoom = false;
        this.controls.enableDamping = false;

        // 조명 설정
        let ambientLight = new THREE.AmbientLight(0x0000ff, 0.15);
        this.scene.add(ambientLight);

        // 조명 설정
        let directionalLight = new THREE.DirectionalLight(0x4400ff, 0.4);
        directionalLight.position.set(5, 3, 2);
        directionalLight.target.position.set(0, 0, 0);
        this.scene.add(directionalLight);
    };

    createItems = () => {
        // 지오메트리
        const imageRatio = 1304 / 2000;
        const imageWidth = 60;
        this.geometry = new THREE.PlaneBufferGeometry(
            imageWidth,
            imageWidth * imageRatio,
            16 * 2,
            9 * 2
        );

        // 머터리얼 만들기
        let shaderMaterial = new THREE.ShaderMaterial({
            uniforms: this.uniforms,
            vertexShader: document.getElementById("vertexShader").textContent,
            fragmentShader: document.getElementById("fragmentShader").textContent,
            depthTest: false,
            transparent: true,
            vertexColors: true,
            side: THREE.DoubleSide
        });

        const message = "Webstoryboy";
        const shapes = this.font.generateShapes(message, 24);
        const geometry = new THREE.ShapeBufferGeometry(shapes);
        geometry.computeBoundingBox();
        const xMid = -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x);
        const yMid = -0.2 * (geometry.boundingBox.max.y - geometry.boundingBox.min.y);
        geometry.translate(xMid, yMid, 2);

        this.mesh = new THREE.Mesh(geometry, shaderMaterial);
        this.scene.add(this.mesh);

        // 구 만들기
        let blobGeom = new THREE.TorusGeometry(20, 5, 10, 50);
        let blobMat = new THREE.MeshPhongMaterial({
            color: 0xff00ff,
            specular: 0x0000ff,
            shininess: 40
        });
        this.blobMesh = new THREE.Mesh(blobGeom, blobMat);
        this.scene.add(this.blobMesh);

        // 카메라 조정
        this.updateCamera();
    };

    addEventListeners = () => {
        this.appContainer.addEventListener("mousemove", this.onMouseMove);
    };

    onMouseMove = (evt) => {
        if (this.targetMeshScale <= this.settings.blobMaxScale) {
            this.targetMeshScale += this.settings.blobInflate;
        }

        let vec = new THREE.Vector3();
        let pos = new THREE.Vector3();
        vec.set(
            (evt.clientX / window.innerWidth) * 2 - 1,
            -(evt.clientY / window.innerHeight) * 2 + 1,
            0.5
        );
        vec.unproject(this.camera);
        vec.sub(this.camera.position).normalize();
        let distance = -this.camera.position.z / vec.z;
        pos.copy(this.camera.position).add(vec.multiplyScalar(distance));

        this.targetMouse = {
            x: pos.x,
            y: pos.y
        };
    };

    updateMouse = () => {
        const mouseDiffX = (this.targetMouse.x - this.currentMouse.x) * this.settings.mouseEase;
        const mouseDiffY = (this.targetMouse.y - this.currentMouse.y) * this.settings.mouseEase;

        this.currentMouse.x += mouseDiffX;
        this.currentMouse.y += mouseDiffY;
    };

    updateItems = () => {
        this.blobMesh.position.set(this.currentMouse.x, this.currentMouse.y, 0);

        const rotateSpeed = Math.abs(this.targetMouse.x - this.currentMouse.x) * 0.002 + 0.01;
        this.blobMesh.rotation.x += rotateSpeed;
        this.blobMesh.rotation.y += rotateSpeed;
        this.blobMesh.rotation.z += rotateSpeed;

        const scaleEffect = (this.targetMeshScale - this.blobMesh.scale.x) * this.settings.blobScaleEase;
        this.blobMesh.scale.set(
            this.blobMesh.scale.x + scaleEffect,
            this.blobMesh.scale.x + scaleEffect,
            this.blobMesh.scale.x + scaleEffect
        );

        if (this.targetMeshScale > this.settings.blobMinScale) {
            this.targetMeshScale -= this.settings.blobDeflate;
        }
    };

    updateCamera = () => {
        this.camera.aspect = this.appContainer.offsetWidth / this.appContainer.offsetHeight;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(
            this.appContainer.offsetWidth,
            this.appContainer.offsetHeight
        );

        const dist = this.camera.position.distanceTo(this.mesh.position);
        const w = this.mesh.geometry.boundingBox.max.x - this.mesh.geometry.boundingBox.min.x;
        const aspect = this.appContainer.offsetWidth / this.appContainer.offsetHeight;
        const fov = 2 * Math.atan(w / aspect / (2 * dist)) * (180 / Math.PI); // in degrees
        this.camera.fov = fov * 1.2;
    };

    update = () => {
        this.iter++;
        this.updateMouse();
        this.updateUniforms();
        this.updateCamera();
        this.updateItems();
        this.renderer.render(this.scene, this.camera);
        window.requestAnimationFrame(this.update);
    };
}

new ThreeText();