canvas#mimiCanvas
View Compiled
body {
	width: 100vw;
	height: 100vh;
	background-image: linear-gradient(#31a2f2, #225af6);
}

canvas {
	vertical-align: middle;
}
View Compiled
"use strict";

import * as THREE from "https://cdn.skypack.dev/[email protected]";
import gsap from "https://cdn.skypack.dev/[email protected]";

// DELETEME: Just for modelling
// import {
// 	OrbitControls
// } from "https://cdn.skypack.dev/[email protected]/examples/jsm/controls/OrbitControls.js";

console.clear();

const IS_DEBUG = false;

const ASSETS_PATH = "https://assets.codepen.io/430361";

const mimiCanvas = document.getElementById("mimiCanvas");

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
	45, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({
	canvas: mimiCanvas,
	alpha: true,
});

// DELETEME: Just for modelling
// const controls = new OrbitControls(camera, mimiCanvas);
// controls.target.set(0, 0, 0);
// controls.update();

const mimiModel = new THREE.Group();

const tl = gsap.timeline({
	repeat: -1,
	delay: 1,
	repeatDelay: 2,
});

function getRadian(degree) {
	return degree * Math.PI / 180;
}

function setRenderer() {
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(window.innerWidth, window.innerHeight);
	
	renderer.shadowMap.enabled = true;
	renderer.shadowMap.type = THREE.PCFSoftShadowMap;
}

function setLighting() {
	const ambientColor = 0xFFFFFF;
  const ambientIntensity = 0.7;
  const ambientLight = new THREE.AmbientLight(ambientColor, ambientIntensity);
  
  scene.add(ambientLight);
	
	const directionalColor = 0xFFFFFF;
  const directionalIntensity = 1;
  const directionalLight = new THREE.DirectionalLight(
		directionalColor, directionalIntensity);
	const directionalX = -1;
	const directionalY = 1;
	const directionalZ = 2;
  
  directionalLight.position.set(directionalX, directionalY, directionalZ);
	directionalLight.castShadow = true;
	
  scene.add(directionalLight);
  scene.add(directionalLight.target);
	
	if (IS_DEBUG) {
		scene.add(new THREE.CameraHelper(directionalLight.shadow.camera));
	}
}

////////////////////////////////////////////////////////////////////////////////
//                               MODELLING START                              //
////////////////////////////////////////////////////////////////////////////////

function createMesh(geometry, color) {
	const material = new THREE.MeshStandardMaterial({
		color: (typeof color === "number") ? color : 0xff0000,
		roughness: 0.8,
		side: THREE.DoubleSide,
	});
	
	const mesh = new THREE.Mesh(geometry, material);
	
	return mesh;
}

function createMeshTexture(geometry, textureName, color) {
	return new Promise(resolve => {
		const loader = new THREE.TextureLoader();
		
		loader.load(`${ASSETS_PATH}/${textureName}`, texture => {
			const material = new THREE.MeshStandardMaterial({
				map: texture,
				roughness: 0.8,
				side: THREE.DoubleSide,
			});
			const mesh = new THREE.Mesh(geometry, material);
			
			resolve(mesh);
		});
	});
}

function createSphere(opt, texture) {
	// Default value is based on ThreeJS documentation
	// Except widthSegments and heightSegments
	opt = {
		radius: opt.radius !== undefined ? opt.radius : 1,
		widthSegments: opt.widthSegments !== undefined ? opt.widthSegments : 16,
		heightSegments: opt.heightSegments !== undefined ? opt.heightSegments : 12,
		phiStart: opt.phiStart !== undefined ? opt.phiStart : 0,
		phiLength: opt.phiLength !== undefined ? opt.phiLength : Math.PI * 2,
		thetaStart: opt.thetaStart !== undefined ? opt.thetaStart : 0,
		thetaLength: opt.thetaLength !== undefined ? opt.thetaLength : Math.PI,
	};
	
	const geometry = new THREE.SphereGeometry(
		opt.radius,
		opt.widthSegments,
		opt.heightSegments,
		opt.phiStart,
		opt.phiLength,
		opt.thetaStart,
		opt.thetaLength,
	);
	
	if (typeof texture === "number") {
		return createMesh(geometry, texture);
	}
	
	return new Promise(async (resolve) => {
		resolve(createMeshTexture(geometry, texture));
	});
}

function createCylinder(opt, texture) {
	// Default value is based on ThreeJS documentation
	// Except radialSegments
	opt = {
		radiusTop: opt.radiusTop != undefined ? opt.radiusTop : 1,
		radiusBottom: opt.radiusBottom != undefined ? opt.radiusBottom : 1,
		height: opt.height != undefined ? opt.height : 1,
		radialSegments: opt.radialSegments != undefined ? opt.radialSegments : 16,
		heightSegments: opt.heightSegments != undefined ? opt.heightSegments : 1,
		openEnded: opt.openEnded != undefined ? opt.openEnded : false,
		thetaStart: opt.thetaStart != undefined ? opt.thetaStart : 0,
		thetaLength: opt.thetaLength != undefined ? opt.thetaLength : Math.PI * 2,
	};
	
	const geometry = new THREE.CylinderGeometry(
		opt.radiusTop,
		opt.radiusBottom,
		opt.height,
		opt.radialSegments,
		opt.heightSegments,
		opt.openEnded,
		opt.thetaStart,
		opt.thetaLength,
	);
	
	if (typeof texture === "number") {
		return createMesh(geometry, texture);
	}
	
	return new Promise(async (resolve) => {
		resolve(createMeshTexture(geometry, texture));
	});
}

function createBox(opt, texture) {
	// Default value is based on ThreeJS documentation
	opt = {
		width: opt.width != undefined ? opt.width : 1,
		height: opt.height != undefined ? opt.height : 1,
		depth: opt.depth != undefined ? opt.depth : 1,
		widthSegments: opt.widthSegments != undefined ? opt.widthSegments : 1,
		heightSegments: opt.heightSegments != undefined ? opt.heightSegments : 1,
		depthSegments: opt.depthSegments != undefined ? opt.depthSegments : 1,
	};
	
	const geometry = new THREE.BoxGeometry(
		opt.width,
		opt.height,
		opt.depth,
		opt.widthSegments,
		opt.heightSegments,
		opt.depthSegments,
	);
	
	if (typeof texture === "number") {
		return createMesh(geometry, texture);
	}
	
	return new Promise(async (resolve) => {
		resolve(createMeshTexture(geometry, texture));
	});
}

function createPlane(opt, texture) {
	// Default value is based on ThreeJS documentation
	opt = {
		width: opt.width != undefined ? opt.width : 1,
		height: opt.height != undefined ? opt.height : 1,
		widthSegments: opt.widthSegments != undefined ? opt.widthSegments : 1,
		heightSegments: opt.heightSegments != undefined ? opt.heightSegments : 1,
	};
	
	const geometry = new THREE.PlaneGeometry(
		opt.width,
		opt.height,
		opt.widthSegments,
		opt.heightSegments,
	);
	
	if (typeof texture === "number") {
		return createMesh(geometry, texture);
	}
	
	return new Promise(async (resolve) => {
		resolve(createMeshTexture(geometry, texture));
	});
}

function createCircle(opt, texture) {
	// Default value is based on ThreeJS documentation
	// Except segments
	opt = {
		radius: opt.radius !== undefined ? opt.radius : 1,
		segments: opt.segments !== undefined ? opt.segments : 16,
		thetaStart: opt.thetaStart !== undefined ? opt.thetaStart : 0,
		thetaLength: opt.thetaLength !== undefined ? opt.thetaLength : Math.PI,
	};
	
	const geometry = new THREE.CircleGeometry(
		opt.radius,
		opt.segments,
		opt.thetaStart,
		opt.thetaLength,
	);
	
	if (typeof texture === "number") {
		return createMesh(geometry, texture);
	}
	
	return new Promise(async (resolve) => {
		resolve(createMeshTexture(geometry, texture));
	});
}

async function createHead() {
	const head = new THREE.Group();
	
	return new Promise(async (resolve) => {
		const face = await createSphere({
			radius: 0.3,
			phiStart: getRadian(-90),
		}, "mimi-face.png");
		const leftEar = createCylinder({
			radiusTop: 0,
			radiusBottom: 0.15,
			height: 0.29,
			radialSegments: 3,
			thetaStart: getRadian(2 / 3 * 100),
		}, 0xcccccc);
		
		leftEar.position.y = 0.27;
		leftEar.position.x = 0.24;
		leftEar.position.z = 0.1;
		leftEar.rotation.z = getRadian(-35);
		leftEar.rotation.x = getRadian(15);
		
		const rightEar = leftEar.clone();
		
		rightEar.position.x = -0.24;
		rightEar.rotation.z = getRadian(35);
		
		head.add(face, leftEar, rightEar);
		
		resolve(head);
	});
}

async function createBody() {
	const body = new THREE.Group();
	
	return new Promise(async (resolve) => {
		const topBody = await createCylinder({
			radiusTop: 0.1,
			radiusBottom: 0.2,
			height: 0.4,
			radialSegments: 16,
			thetaStart: getRadian(180),
		}, "mimi-body.png");
		const bottomBody = createSphere({
			radius: 0.2825,
			phiStart: 0,
			thetaStart: getRadian(135),
			thetaLength: getRadian(45),
		}, 0x493c2b);
		
		bottomBody.position.y = 0;
		
		body.add(topBody, bottomBody);
		
		resolve(body);
	});
}

async function createArms() {
	const arms = new THREE.Group();
	
	return new Promise(async (resolve) => {
		const leftArm = new THREE.Group();
		const leftShoulder = createSphere({ radius: 0.05 }, 0xa46422);
		const leftArm1 = createCylinder({
			radiusTop: 0.05,
			radiusBottom: 0.05,
			height: 0.1,
		}, 0xa46422);
		const leftElbow = leftShoulder.clone();
		const leftArm2 = leftArm1.clone();
		const leftHand = createSphere({ radius: 0.075 }, 0xcccccc);
		
		leftArm.position.x = 0.12;
		leftArm.position.y = -0.13;
		leftArm.rotation.z = getRadian(-45);
		leftArm.add(leftShoulder);
		
		leftShoulder.add(leftArm1);
		
		leftArm1.rotation.z = getRadian(90);
		leftArm1.position.x = 0.05;
		leftArm1.add(leftElbow);
		
		leftElbow.position.y = -0.05;
		leftElbow.add(leftArm2);
		
		leftArm2.position.y = -0.05;
		leftArm2.add(leftHand);
		
		leftHand.position.y = -0.11;
		
		const rightArm = leftArm.clone();
		
		rightArm.position.x = -0.12;
		rightArm.rotation.z = getRadian(225);
		
		arms.add(leftArm, rightArm);
		
		resolve(arms);
	});
}

async function createLegs() {
	const legs = new THREE.Group();
	
	return new Promise(async (resolve) => {
		const leftLeg = new THREE.Group();
		const leftHip = createSphere({ radius: 0.05 }, 0x493c2b);
		const leftLeg1 = createCylinder({
			radiusTop: 0.05,
			radiusBottom: 0.05,
			height: 0.1,
		}, 0x493c2b);
		const leftKnee = leftHip.clone();
		const leftLeg2 = leftLeg1.clone();
		const leftShoe1 = createCylinder({
			radiusTop: 0.06,
			radiusBottom: 0.06,
			height: 0.15,
			thetaLength: Math.PI,
		}, 0x1b2632);
		const leftShoe2 = createSphere({
			radius: 0.06,
			phiStart: Math.PI / 2,
			phiLength: Math.PI,
		}, 0x1b2632);
		const leftSole1 = createPlane({
			width: 0.12,
			height: 0.15,
		}, 0x1b2632);
		const leftSole2 = createCircle({
			radius: 0.06,
			thetaLength: Math.PI,
		}, 0x1b2632);
		
		leftLeg.position.y = -0.46;
		leftLeg.position.x = 0.09;
		leftLeg.add(leftHip);
		
		leftHip.add(leftLeg1);
		
		leftLeg1.position.y = -0.05;
		leftLeg1.add(leftKnee);
		
		leftKnee.position.y = -0.05;
		leftKnee.add(leftLeg2);
		
		leftLeg2.position.y = -0.05;
		leftLeg2.add(leftShoe1);
		
		leftShoe1.position.y = -0.1;
		leftShoe1.position.z = 0.01;
		leftShoe1.rotation.x = getRadian(90);
		leftShoe1.rotation.y = getRadian(90);
		leftShoe1.add(leftShoe2);
		leftShoe1.add(leftSole1);
		
		leftShoe2.position.y = 0.08;
		leftShoe2.add(leftSole2);
		
		leftSole1.rotation.y = getRadian(90);
		
		leftSole2.rotation.y = getRadian(-90);
		
		const rightLeg = leftLeg.clone();
		
		rightLeg.position.x = -0.09;
		
		legs.add(leftLeg, rightLeg);
		
		resolve(legs);
	});
}

function createModel() {
	return new Promise(async (resolve) => {
		const head = await createHead();
		const body = await createBody();
		const arms = await createArms();
		const legs = await createLegs();

		head.position.y = 0.2;
		body.position.y = -0.2;

		mimiModel.add(head, body, arms, legs);

		mimiModel.castShadow = true;

		scene.add(mimiModel);
		
		resolve();
	});
}

////////////////////////////////////////////////////////////////////////////////
//                                MODELLING END                               //
////////////////////////////////////////////////////////////////////////////////

function addPlatform() {
	return new Promise(async (resolve) => {
		const platform = await createPlane({
			width: 2,
			height: 2,
		}, "infinite-cars-grass.png");

		platform.rotation.x = Math.PI / 2;
		platform.position.y = -0.72;

		platform.receiveShadow = true;

		scene.add(platform);
		
		resolve();
	});
}

////////////////////////////////////////////////////////////////////////////////
//                               ANIMATION START                              //
////////////////////////////////////////////////////////////////////////////////

function animateArms(arms) {
	const rightArm = arms.children[1];
	const rightShoulder = rightArm.children[0];
	const rightElbow = rightShoulder.children[0].children[0];
	
	// rightElbow.rotation.z = getRadian(-30);
	
	tl.to(rightShoulder.rotation, {
		z: getRadian(-70),
		ease: "linear",
		duration: 0.5,
	}, 0);
	
	tl.to(rightElbow.rotation, {
		z: getRadian(-45),
		ease: "linear",
		duration: 0.4,
		repeat: 5,
		yoyo: true,
		yoyoEase: true,
	});
	
	tl.to(rightShoulder.rotation, {
		z: 0,
		ease: "linear",
		duration: 0.3,
	});
}

function animateLegs(legs) {
	const leftLeg = legs.children[0];
	const leftHip = leftLeg.children[0];
	
	tl.to(leftHip.rotation, {
		z: getRadian(25),
		duration: 0.2,
	}, 0);
	
	tl.to(leftHip.rotation, {
		z: 0,
		ease: "linear",
		duration: 0.1,
	}, 2.7);
}

function animateBody() {
	tl.to(mimiModel.position, {
		y: 0.1,
		ease: "power2.out",
		duration: 0.2,
	}, 0);
	
	tl.to(mimiModel.position, {
		y: 0,
		ease: "power2.in",
		duration: 0.1,
	}, 0.2);
	
	tl.to(mimiModel.rotation, {
		z: getRadian(-25),
		ease: "power2.out",
		duration: 0.2,
	}, 0);
	
	tl.to(mimiModel.position, {
		y: 0.1,
		ease: "power2.out",
		duration: 0.2,
	}, 2.7);
	
	tl.to(mimiModel.position, {
		y: 0,
		ease: "power2.in",
		duration: 0.1,
	}, 2.9);
	
	tl.to(mimiModel.rotation, {
		z: 0,
		ease: "power2.out",
		duration: 0.2,
	}, 2.7);
}

function setAnimation() {
	animateArms(mimiModel.children[2]);
	animateLegs(mimiModel.children[3]);
	animateBody();
}

////////////////////////////////////////////////////////////////////////////////
//                                ANIMATION END                               //
////////////////////////////////////////////////////////////////////////////////

function update() {
	requestAnimationFrame(update);
	
	renderer.render(scene, camera);
	
	mimiModel.rotation.y += 0.0075;
	
	// DELETEME: Just for modelling
	// controls.update();
}

async function initialize() {
	setRenderer();
	
	// Initialize camera position
	camera.position.z = 2.5;
	
	if (IS_DEBUG) {
		scene.add(new THREE.CameraHelper(camera));
	}
	
	setLighting();
	
	await createModel();
	// await addPlatform();
	
	setAnimation();
	update();
}

initialize();

window.addEventListener("resize", evt => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  
  renderer.setSize(window.innerWidth, window.innerHeight);
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.