import splt from "https://esm.sh/spltjs";
import { animate, press, hover, stagger } from "https://esm.sh/motion";
import { Howl } from "https://esm.sh/howler";
import VanillaTilt from "https://esm.sh/vanilla-tilt";
import AudioMotionAnalyzer from "https://cdn.skypack.dev/audiomotion-analyzer?min";
VanillaTilt.init(document.querySelector(".card-wrap"), {
speed: 50,
max: 5,
glare: true,
"max-glare": 0.05
});
splt({ reveal: true });
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let playAudio = true,
ended = true,
currentSound;
const play = async (file, volume = 0.5, speed = 1) => {
await audioCtx.resume().catch(() => {});
if (audioCtx.state === "suspended") return false;
currentSound?.stop();
ended = false;
currentSound = new Howl({
src: [file],
autoplay: playAudio,
volume,
rate: speed,
onend: () => {
ended = true;
currentSound = null;
}
});
return true;
};
const audioEl = new Audio(
"https://cdn.jsdelivr.net/gh/ouyahama/cdn@main/land.mp3"
);
audioEl.crossOrigin = "anonymous";
press(".card", () => {
animate(".card", { scale: 1.05 });
audioEl.play();
return () => {
animate(".card", { scale: 1 });
};
});
// audio visulizer
const audioMotion = new AudioMotionAnalyzer(document.getElementById("wave"), {
source: audioEl,
mode: "led",
ledBars: true,
showBgColor: false,
showFPS: false,
showPeaks: false,
showScaleX: false,
showScaleY: false,
gradient: "prism",
mode: 1,
colorMode: "bar"
});
// -----intro animation------
animate(".thumbnail", { opacity: 0 });
(async () => {
animate(".made", { opacity: [0, 0.75] }, { duration: 0.5 });
animate(".small", { opacity: [0, 0.75] }, { duration: 0.5 });
animate(
".card",
{ filter: ["blur(1rem)", "blur(0)"], scale: [0.8, 1], opacity: [0, 1] },
{ duration: 0.3 }
);
await animate(
".logo",
{ scale: [0, 1], opacity: [0, 1] },
{ duration: 0.2, delay: 0.1 }
);
animate(
".right .on",
{ scale: [0.8, 1], opacity: [0, 1] },
{ duration: 0.2 }
);
animate(
".right .text",
{ scale: [0.8, 1], opacity: [0, 1] },
{ duration: 0.3 }
);
animate(
".right .flight",
{ scale: [0.8, 1], opacity: [0, 1] },
{ duration: 0.3 }
);
await animate(
".label",
{ scale: [0.8, 1], opacity: [0, 0.5] },
{ duration: 0.3 }
);
await animate(
".box",
{ filter: ["blur(1rem)", "blur(0)"], scale: [0.8, 1], opacity: [0, 1] },
{ duration: 0.3, delay: stagger(0.1) }
);
await animate(
".dur",
{ scale: [0, 1], z: 20, opacity: [0, 1] },
{ duration: 0.3 }
);
await animate(
".speed",
{ scale: [0.8, 1], opacity: [0, 0.5] },
{ duration: 0.3, delay: stagger(0.1) }
);
await animate(
".plane",
{ scale: [0.8, 1], opacity: [0, 0.9] },
{ duration: 0.3 }
);
await animate(
".item",
{ scale: [0.8, 1], opacity: [0, 1] },
{ duration: 0.3, delay: stagger(0.1) }
);
})();
document.addEventListener("mousemove", (e) => {
const tooltips = document.querySelectorAll(".info");
tooltips.forEach((tooltip) => {
tooltip.style.left = `${e.clientX + 20}px`;
tooltip.style.top = `${e.clientY + 20}px`;
});
});
hover(".box.to", () => {
animate(
".info.to",
{
opacity: 1,
scale: 1,
filter: "blur(0)"
},
{ duration: 0.4 }
);
return () => {
animate(
".info.to",
{
opacity: 0,
scale: 0,
filter: "blur(1rem)"
},
{ duration: 0.4 }
);
};
});
hover(".box.from", () => {
animate(
".info.from",
{
opacity: 1,
scale: 1,
filter: "blur(0)"
},
{ duration: 0.4 }
);
return () => {
animate(
".info.from",
{
opacity: 0,
scale: 0,
filter: "blur(1rem)"
},
{ duration: 0.4 }
);
};
});
window.addEventListener("DOMContentLoaded", async () => {
const [
THREE,
{ OrbitControls },
{ GLTFLoader },
{ DRACOLoader },
{ RGBELoader },
{ EffectComposer },
{ RenderPass },
{ ShaderPass },
{ BrightnessContrastShader }
] = await Promise.all([
import("https://esm.sh/three@0.178.0"),
import("https://esm.sh/three@0.178.0/examples/jsm/controls/OrbitControls"),
import("https://esm.sh/three@0.178.0/examples/jsm/loaders/GLTFLoader"),
import("https://esm.sh/three@0.178.0/examples/jsm/loaders/DRACOLoader"),
import("https://esm.sh/three@0.178.0/examples/jsm/loaders/RGBELoader"),
import(
"https://esm.sh/three@0.178.0/examples/jsm/postprocessing/EffectComposer"
),
import(
"https://esm.sh/three@0.178.0/examples/jsm/postprocessing/RenderPass"
),
import(
"https://esm.sh/three@0.178.0/examples/jsm/postprocessing/ShaderPass"
),
import(
"https://esm.sh/three@0.178.0/examples/jsm/shaders/BrightnessContrastShader"
)
]);
const canvas = document.querySelector("#plane");
const { width, height } = canvas.getBoundingClientRect();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000);
camera.position.set(40, 20, 70);
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true
});
renderer.setSize(width, height, false);
renderer.setPixelRatio(devicePixelRatio);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.8;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = false;
controls.enablePan = false;
controls.enableRotate = false;
controls.autoRotate = false;
const rimLight = new THREE.DirectionalLight(0xffffff, 0.1);
rimLight.position.set(-5, 5, 10);
scene.add(rimLight, new THREE.AmbientLight(0xffffff, 0.2));
const modelGroup = new THREE.Group();
const baseYaw = Math.PI / 1.6;
scene.add(modelGroup);
hover(".card", () => {
animate(modelGroup.scale, { x: 1.05, y: 1.05, z: 1.05 }, { duration: 0.4 });
return () => {
animate(modelGroup.scale, { x: 1, y: 1, z: 1 }, { duration: 0.4 });
};
});
const pmrem = new THREE.PMREMGenerator(renderer);
pmrem.compileEquirectangularShader();
new RGBELoader()
.setPath("https://cdn.jsdelivr.net/gh/ouyahama/cdn@main/")
.load("light3.hdr", (hdr) => {
const envMap = pmrem.fromEquirectangular(hdr).texture;
scene.environment = envMap;
hdr.dispose();
pmrem.dispose();
});
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
dracoLoader.setDecoderConfig({ type: "js" });
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.load(
"https://cdn.jsdelivr.net/gh/ouyahama/cdn@main/plane.glb",
(gltf) => {
gltf.scene.traverse((child) => {
if (child.isMesh) {
const mat = child.material;
if (mat.isMeshStandardMaterial || mat.isMeshPhysicalMaterial) {
mat.roughness = 0.4;
mat.metalness = 0.7;
mat.envMapIntensity = 0.9;
}
}
});
modelGroup.add(gltf.scene);
},
undefined,
console.error
);
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const brightnessContrast = new ShaderPass(BrightnessContrastShader);
brightnessContrast.uniforms["brightness"].value = 0;
brightnessContrast.uniforms["contrast"].value = 0;
composer.addPass(brightnessContrast);
const clock = new THREE.Clock();
const renderLoop = () => {
requestAnimationFrame(renderLoop);
const t = clock.getElapsedTime();
const yaw = baseYaw + Math.sin(t * 0.25) * 0.1;
const pitch = Math.sin(t * 0.35) * 0.05;
const roll = Math.sin(t * 0.5) * 0.07;
modelGroup.rotation.set(pitch, yaw, roll);
controls.update();
composer.render();
};
requestAnimationFrame(() => {
renderLoop();
});
animate(canvas, { opacity: [0, 1] }, { duration: 1, delay: 0.2 });
window.addEventListener("resize", () => {
const { width, height } = canvas.getBoundingClientRect();
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
composer.setSize(width, height);
});
});
let s = 290;
setInterval(() => {
const n = Math.min(306, Math.max(278, s + (Math.random() * 6 - 3)));
animate(s, n, {
duration: 0.5,
ease: "circOut",
onUpdate: (v) =>
(document.querySelector(".speed-value").innerHTML = Math.round(v))
});
s = n;
}, 500);
let alt = 5500; // altitude in meters
setInterval(() => {
const descentRate = 4.5; // slow descent ~540 m/min
const fluctuation = Math.random() * 6 - 3; // small noise ±3m
const nextAlt = Math.max(0, alt - descentRate + fluctuation);
animate(alt, nextAlt, {
duration: 0.5,
ease: "circOut",
onUpdate: (v) =>
(document.querySelector(".altitude-value").innerHTML = Math.round(v))
});
alt = nextAlt;
}, 500);
let pitch = 3.5; // start with mild nose-down
setInterval(() => {
const leveling = -0.01; // subtle leveling trend
const fluctuation = Math.random() * 0.2 - 0.1; // small noise
const nextPitch = Math.min(5, Math.max(2, pitch + leveling + fluctuation));
animate(pitch, nextPitch, {
duration: 0.5,
ease: "circOut",
onUpdate: (v) =>
(document.querySelector(".pitch-value").innerHTML = v.toFixed(1))
});
pitch = nextPitch;
}, 500);
hover(".made", () => {
animate(
".made",
{ scale: 1.02, opacity: 1 },
{ type: "spring", stiffness: 200 }
);
return () =>
animate(".made", { scale: 1, opacity: 0.75 }, { type: "spring" });
});
hover(".follow", () => {
animate(
".follow",
{ scale: 1.02, opacity: 1 },
{ type: "spring", stiffness: 200 }
);
return () =>
animate(".follow", { scale: 1, opacity: 0.75 }, { type: "spring" });
});
!