# 360 landing page using three.js and nuxt.js

**URL:** <https://discourse.threejs.org/t/360-landing-page-using-three-js-and-nuxt-js/56614>\
**Category:** Showcase\
**Tags:** 360°-images, 360°, vue, nuxt, threejs\
**Created:** [October 3, 2023, 1:06pm UTC](https://discourse.threejs.org/t/360-landing-page-using-three-js-and-nuxt-js/56614 "2023-10-03T13:06:30Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![sumer5020](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/sumer5020/32/40524_2.png) [@sumer5020](https://discourse.threejs.org/u/sumer5020)\
**Post date:** [October 3, 2023, 1:06pm UTC](https://discourse.threejs.org/t/360-landing-page-using-three-js-and-nuxt-js/56614/1 "2023-10-03T13:06:30Z")

</div>

Hi all, i like to share how to create basic 360 landing page using three.js, nuxt.js and vue3

## install new nuxt.js project

```javascript
npx nuxi@latest init simple-app
cd simple-app
npm i

```

## install three.js

```javascript
npm i three -D

```

now we are ready to go let’s create new component for our 360 landing page

## create new component

```javascript
npx nuxi add component Landing

```

now we will create the component content like this

```javascript
<script setup lang="ts">
import {
PerspectiveCamera,
Scene,
SphereGeometry,
TextureLoader,
MeshBasicMaterial,
Mesh,
WebGLRenderer,
MathUtils,
} from "three";

const container = ref()

const props = defineProps({
    panoramaUrl:{
        type: String,
        default: "img/hdri.webp"
    },
});

let camera, scene, renderer;
let isUserInteracting = false,
    onPointerDownMouseX = 0, onPointerDownMouseY = 0,
    lon = 0, onPointerDownLon = 0,
    lat = 0, onPointerDownLat = 0,
    phi = 0, theta = 0;

camera = new PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1100);
scene = new Scene();
const geometry = new SphereGeometry(1000, 100, 100);
const reader = new FileReader();

const onWindowResize= () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

const onPointerDown = (event) => {
    if (event.isPrimary === false) return;
    isUserInteracting = true;
    onPointerDownMouseX = event.clientX;
    onPointerDownMouseY = event.clientY;
    onPointerDownLon = lon;
    onPointerDownLat = lat;
    document.addEventListener('pointermove', onPointerMove);
    document.addEventListener('pointerup', onPointerUp);
}

const onPointerMove = (event) => {
    if (event.isPrimary === false) return;
    lon = (onPointerDownMouseX - event.clientX) * 0.1 + onPointerDownLon;
    lat = (event.clientY - onPointerDownMouseY) * 0.1 + onPointerDownLat;
}

const onPointerUp = () => {
    if (event.isPrimary === false) return;
    isUserInteracting = false;
    document.removeEventListener('pointermove', onPointerMove);
    document.removeEventListener('pointerup', onPointerUp);
}

const onDocumentMouseWheel = (event) => {
    const fov = camera.fov + event.deltaY * 0.05;
    camera.fov = MathUtils.clamp(fov, 10, 75);
    camera.updateProjectionMatrix();
}

const animate = () => {
    requestAnimationFrame(animate);
    update();
}

const update = () => {
    if (isUserInteracting === false) {
        lon += 0.1;
    }

    lat = Math.max(- 85, Math.min(85, lat));
    phi = MathUtils.degToRad(90 - lat);
    theta = MathUtils.degToRad(lon);
    const x = 500 * Math.sin(phi) * Math.cos(theta);
    const y = 500 * Math.cos(phi);
    const z = 500 * Math.sin(phi) * Math.sin(theta);
    camera.lookAt(x, y, z);
    renderer.render(scene, camera);
}

const dragover = (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'copy';
}

const dragenter = () => {
    document.body.style.opacity = 0.5;
}

const dragleave = () => {
    document.body.style.opacity = 1;
}

const load1 = (event) => {
    material.map.image.src = event.target.result;
    material.map.needsUpdate = true;
}

const drop = () => {
    event.preventDefault();
    reader.addEventListener('load', load1);
    reader.readAsDataURL(event.dataTransfer.files[0]);
    document.body.style.opacity = 1;
}

onMounted(() => {
        init();
        animate();

        function init() {
            // invert the geometry on the x-axis so that all of the faces point inward
            geometry.scale(- 1, 1, 1);
            const texture = new TextureLoader().load(props.panoramaUrl);
            const material = new MeshBasicMaterial({ map: texture });
            const mesh = new Mesh(geometry, material);
            scene.add(mesh);
            renderer = new WebGLRenderer();
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(window.innerWidth, window.innerHeight);
            container.value.appendChild(renderer.domElement);
            container.value.style.touchAction = 'none';
            container.value.addEventListener('pointerdown', onPointerDown);
            document.addEventListener('wheel', onDocumentMouseWheel);
            document.addEventListener('dragover', dragover);
            document.addEventListener('dragenter', dragenter);
            document.addEventListener('dragleave', dragleave);
            document.addEventListener('drop', drop);
            window.addEventListener('resize', onWindowResize);
        }
});

onUnmounted(() => {
    reader.removeEventListener('load', load1);
    container.value.replaceWith(container.value.cloneNode(true));
    document.removeEventListener('wheel', onDocumentMouseWheel);
    document.removeEventListener('dragover', dragover);
    document.removeEventListener('dragenter', dragenter);
    document.removeEventListener('dragleave', dragleave);
    document.removeEventListener('drop', drop);
    window.removeEventListener('resize', onWindowResize);
});
</script>

<template>
    <div class="max-w-full h-screen w-full overflow-hidden">
        <div ref="container"></div>
    </div>
</template>

```

Note that: you can find info about each part of the code in the official documentation of three.js in the link below.  
[three.js documentation](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene)

now we need to create folder inside the `public` with the name `img` then we will create our image that use `hdri` mode.  
You can create your own image or you can find one from any resource in this part i basically use greater website called `HDRi Haven` then i convert the image to webp just to minify the total size of the image also in my case i don’t care about the shadow reflex and that other powerful things.  
Now we have the image `img/hdri.webp` in our public folder we are ready to use our component in the index.vue page.

Note that: we may have some rendering problem if we use `SSR` with our app so basically we will use `<ClientOnly>` wen we call our component like bellow.

```javascript
<ClientOnly>
    <Landing/>
    <template #fallback>
      <!-- this will be rendered on server side -->
      <p>Landing is loading...</p>
    </template>
  </ClientOnly>

```

That’s it, we have now a basic 360 landing page
