Olá! Claro, aqui está uma descrição da aplicação que criámos.
Browse filesEsta é uma **simulação de uma nebulosa de partículas 3D interativa**, construída com a biblioteca `three.js`. O objetivo é criar uma experiência visualmente imersiva e personalizável de exploração espacial.
As suas principais características são:
* **Visualização 3D:** A aplicação renderiza uma nebulosa composta por dezenas de milhares de partículas. Estas partículas estão distribuídas em várias camadas e têm cores diferentes, o que cria uma forte sensação de profundidade e volume. Um efeito de "brilho" (bloom) é aplicado para dar um aspeto mais etéreo e luminoso à cena.
* **Interatividade Múltipla:** A nebulosa reage a várias ações do utilizador em tempo real:
* **Navegação da Câmara:** Pode usar o rato para rodar, aproximar/afastar o zoom e mover a câmara, explorando a nebulosa de qualquer ângulo. As partículas reagem à proximidade da câmara, sendo suavemente afastadas.
* **Reatividade ao Áudio:** Ao clicar em "Ativar Áudio", a aplicação utiliza o microfone para analisar a sua voz ou o som ambiente. As partículas pulsam e mudam de cor em sintonia com a intensidade e as frequências graves do som.
* **Interação por Clique:** Clicar em qualquer ponto do espaço cria uma onda de choque visual que se propaga pela nebulosa, perturbando as partículas.
* **Painel de Configurações Avançado:** Ao clicar no ícone de engrenagem, abre-se um painel de controlo com várias opções, organizadas por abas:
* **Visual:** Permite ajustar a força da interação, o tamanho geral das partículas e a intensidade do efeito de brilho.
* **Áudio:** Contém um slider para ajustar a sensibilidade da nebulosa ao som.
* **Câmara:** Oferece um botão para repor a câmara na sua posição inicial.
Em suma, é uma peça de arte digital interativa que combina gráficos 3D, som e interação do utilizador para criar uma experiência cósmica relaxante e visualmente cativante.
- README.md +9 -5
- index.html +806 -18
|
@@ -1,10 +1,14 @@
|
|
| 1 |
---
|
| 2 |
-
title: Nebulosa
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Nebulosa Cósmica Interativa ✨
|
| 3 |
+
colorFrom: yellow
|
| 4 |
+
colorTo: yellow
|
| 5 |
+
emoji: 🐳
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite-v3
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Welcome to your new DeepSite project!
|
| 13 |
+
This project was created with [DeepSite](https://deepsite.hf.co).
|
| 14 |
+
|
|
@@ -1,19 +1,807 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="pt">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Nebulosa Cósmica Interativa ✨</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--glow-sens: 30;
|
| 10 |
+
--card-bg: linear-gradient(8deg, #140426 75%, color-mix(in hsl, #140426, white 2.5%) 75.5%);
|
| 11 |
+
--blend: soft-light;
|
| 12 |
+
--glow-blend: plus-lighter;
|
| 13 |
+
--glow-color: 268deg 100% 76%;
|
| 14 |
+
--glow-boost: 0%;
|
| 15 |
+
--fg: white;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
* {
|
| 19 |
+
margin: 0;
|
| 20 |
+
padding: 0;
|
| 21 |
+
box-sizing: border-box;
|
| 22 |
+
}
|
| 23 |
+
html, body {
|
| 24 |
+
width: 100%;
|
| 25 |
+
height: 100%;
|
| 26 |
+
overflow: hidden;
|
| 27 |
+
background-color: #020108; /* Cor de fundo para evitar piscar em branco */
|
| 28 |
+
font-family: 'Inter', sans-serif;
|
| 29 |
+
color: var(--fg);
|
| 30 |
+
}
|
| 31 |
+
#container {
|
| 32 |
+
position: fixed;
|
| 33 |
+
width: 100%;
|
| 34 |
+
height: 100%;
|
| 35 |
+
background: radial-gradient(circle at 50% 50%,
|
| 36 |
+
#1a0632 0%,
|
| 37 |
+
#140426 25%,
|
| 38 |
+
#0c021a 50%,
|
| 39 |
+
#06020e 75%,
|
| 40 |
+
#020108 100%
|
| 41 |
+
);
|
| 42 |
+
}
|
| 43 |
+
canvas {
|
| 44 |
+
display: block;
|
| 45 |
+
width: 100%;
|
| 46 |
+
height: 100%;
|
| 47 |
+
}
|
| 48 |
+
.glow-overlay {
|
| 49 |
+
position: fixed;
|
| 50 |
+
top: 0;
|
| 51 |
+
left: 0;
|
| 52 |
+
width: 100%;
|
| 53 |
+
height: 100%;
|
| 54 |
+
pointer-events: none;
|
| 55 |
+
background: radial-gradient(circle at 50% 50%,
|
| 56 |
+
rgba(120, 50, 255, 0.05) 0%,
|
| 57 |
+
rgba(80, 40, 200, 0.03) 40%,
|
| 58 |
+
transparent 70%);
|
| 59 |
+
mix-blend-mode: screen;
|
| 60 |
+
}
|
| 61 |
+
#audio-controls {
|
| 62 |
+
position: fixed;
|
| 63 |
+
top: 20px;
|
| 64 |
+
left: 50%;
|
| 65 |
+
transform: translateX(-50%);
|
| 66 |
+
z-index: 10;
|
| 67 |
+
text-align: center;
|
| 68 |
+
}
|
| 69 |
+
#startButton {
|
| 70 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 71 |
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
| 72 |
+
color: white;
|
| 73 |
+
padding: 12px 24px;
|
| 74 |
+
font-size: 16px;
|
| 75 |
+
border-radius: 8px;
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
transition: background-color 0.3s, transform 0.2s;
|
| 78 |
+
backdrop-filter: blur(5px);
|
| 79 |
+
font-weight: 500;
|
| 80 |
+
letter-spacing: 0.5px;
|
| 81 |
+
}
|
| 82 |
+
#startButton:hover {
|
| 83 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 84 |
+
}
|
| 85 |
+
#startButton:active {
|
| 86 |
+
transform: scale(0.95);
|
| 87 |
+
}
|
| 88 |
+
.title-container {
|
| 89 |
+
position: fixed;
|
| 90 |
+
top: 20px;
|
| 91 |
+
left: 20px;
|
| 92 |
+
z-index: 10;
|
| 93 |
+
text-align: left;
|
| 94 |
+
}
|
| 95 |
+
.main-title {
|
| 96 |
+
font-size: 2.5rem;
|
| 97 |
+
font-weight: 700;
|
| 98 |
+
background: linear-gradient(135deg, #c299ff 0%, #a855f7 50%, #7e22ce 100%);
|
| 99 |
+
-webkit-background-clip: text;
|
| 100 |
+
-webkit-text-fill-color: transparent;
|
| 101 |
+
margin-bottom: 5px;
|
| 102 |
+
text-shadow: 0 0 30px rgba(124, 58, 237, 0.5);
|
| 103 |
+
}
|
| 104 |
+
.subtitle {
|
| 105 |
+
font-size: 1rem;
|
| 106 |
+
color: rgba(255, 255, 255, 0.8);
|
| 107 |
+
font-weight: 300;
|
| 108 |
+
letter-spacing: 1px;
|
| 109 |
+
}
|
| 110 |
+
#settings-container {
|
| 111 |
+
position: fixed;
|
| 112 |
+
bottom: 20px;
|
| 113 |
+
left: 20px;
|
| 114 |
+
z-index: 20;
|
| 115 |
+
}
|
| 116 |
+
#settingsButton {
|
| 117 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 118 |
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
| 119 |
+
color: white;
|
| 120 |
+
width: 48px;
|
| 121 |
+
height: 48px;
|
| 122 |
+
padding: 10px;
|
| 123 |
+
border-radius: 50%;
|
| 124 |
+
cursor: pointer;
|
| 125 |
+
transition: background-color 0.3s, transform 0.2s;
|
| 126 |
+
backdrop-filter: blur(5px);
|
| 127 |
+
display: flex;
|
| 128 |
+
justify-content: center;
|
| 129 |
+
align-items: center;
|
| 130 |
+
}
|
| 131 |
+
#settingsButton:hover {
|
| 132 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 133 |
+
}
|
| 134 |
+
#settingsButton:active {
|
| 135 |
+
transform: scale(0.95);
|
| 136 |
+
}
|
| 137 |
+
#settingsButton svg {
|
| 138 |
+
width: 24px;
|
| 139 |
+
height: 24px;
|
| 140 |
+
fill: currentColor;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* Estilos do Popup */
|
| 144 |
+
.popup-overlay {
|
| 145 |
+
position: fixed;
|
| 146 |
+
inset: 0;
|
| 147 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 148 |
+
backdrop-filter: blur(10px);
|
| 149 |
+
z-index: 100;
|
| 150 |
+
display: grid;
|
| 151 |
+
place-items: center;
|
| 152 |
+
opacity: 0;
|
| 153 |
+
pointer-events: none;
|
| 154 |
+
transition: opacity 0.3s ease;
|
| 155 |
+
}
|
| 156 |
+
.popup-overlay.visible {
|
| 157 |
+
opacity: 1;
|
| 158 |
+
pointer-events: auto;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.card {
|
| 162 |
+
--pads: 20px;
|
| 163 |
+
--color-sens: calc(var(--glow-sens) + 20);
|
| 164 |
+
--pointer-°: 45deg;
|
| 165 |
+
position: relative;
|
| 166 |
+
width: clamp(320px, 90vw, 500px);
|
| 167 |
+
max-height: 90vh;
|
| 168 |
+
border-radius: 1.768em;
|
| 169 |
+
isolation: isolate;
|
| 170 |
+
transform: translate3d(0, 0, 0.01px);
|
| 171 |
+
display: grid;
|
| 172 |
+
border: 1px solid rgb(255 255 255 / 25%);
|
| 173 |
+
background: var(--card-bg);
|
| 174 |
+
background-repeat: no-repeat;
|
| 175 |
+
box-shadow: rgba(0, 0, 0, 0.1) 0px 32px 64px, rgba(0, 0, 0, 0.1) 0px 16px 32px, rgba(0, 0, 0, 0.1) 0px 8px 16px, rgba(0, 0, 0, 0.1) 0px 4px 8px, rgba(0, 0, 0, 0.1) 0px 2px 4px, rgba(0, 0, 0, 0.1) 0px 1px 2px;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.card::before, .card::after, .card > .glow {
|
| 179 |
+
content: "";
|
| 180 |
+
position: absolute;
|
| 181 |
+
inset: 0;
|
| 182 |
+
border-radius: inherit;
|
| 183 |
+
transition: opacity 0.25s ease-out;
|
| 184 |
+
z-index: -1;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.card:not(:hover):not(.animating) .glow,
|
| 188 |
+
.card:not(:hover):not(.animating)::before,
|
| 189 |
+
.card:not(:hover):not(.animating)::after {
|
| 190 |
+
opacity: 0;
|
| 191 |
+
transition: opacity 0.75s ease-in-out;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.card::before {
|
| 195 |
+
border: 1px solid transparent;
|
| 196 |
+
background: linear-gradient(var(--card-bg) 0 100%) padding-box, linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box, radial-gradient(at 80% 55%, hsla(268,100%,76%,1) 0px, transparent 50%) border-box, radial-gradient(at 69% 34%, hsla(349,100%,74%,1) 0px, transparent 50%) border-box, radial-gradient(at 8% 6%, hsla(136,100%,78%,1) 0px, transparent 50%) border-box, radial-gradient(at 41% 38%, hsla(192,100%,64%,1) 0px, transparent 50%) border-box, radial-gradient(at 86% 85%, hsla(186,100%,74%,1) 0px, transparent 50%) border-box, radial-gradient(at 82% 18%, hsla(52,100%,65%,1) 0px, transparent 50%) border-box, radial-gradient(at 51% 4%, hsla(12,100%,72%,1) 0px, transparent 50%) border-box, linear-gradient(#c299ff 0 100%) border-box;
|
| 197 |
+
opacity: calc((var(--pointer-d) - var(--color-sens)) / (100 - var(--color-sens)));
|
| 198 |
+
mask-image: conic-gradient(from var(--pointer-°) at center, black 25%, transparent 40%, transparent 60%, black 75%);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.card > .glow {
|
| 202 |
+
--outset: 20px;
|
| 203 |
+
inset: calc(var(--outset) * -1);
|
| 204 |
+
pointer-events: none;
|
| 205 |
+
z-index: 1;
|
| 206 |
+
mask-image: conic-gradient(from var(--pointer-°) at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%);
|
| 207 |
+
opacity: calc((var(--pointer-d) - var(--glow-sens)) / (100 - var(--glow-sens)));
|
| 208 |
+
mix-blend-mode: var(--glow-blend);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.card > .glow::before {
|
| 212 |
+
content: "";
|
| 213 |
+
position: absolute;
|
| 214 |
+
inset: var(--outset);
|
| 215 |
+
border-radius: inherit;
|
| 216 |
+
box-shadow: inset 0 0 0 1px hsl(var(--glow-color) / 100%), inset 0 0 1px 0 hsl(var(--glow-color) / calc(var(--glow-boost) + 60%)), inset 0 0 3px 0 hsl(var(--glow-color) / calc(var(--glow-boost) + 50%)), inset 0 0 6px 0 hsl(var(--glow-color) / calc(var(--glow-boost) + 40%)), inset 0 0 15px 0 hsl(var(--glow-color) / calc(var(--glow-boost) + 30%)), inset 0 0 25px 2px hsl(var(--glow-color) / calc(var(--glow-boost) + 20%)), inset 0 0 50px 2px hsl(var(--glow-color) / calc(var(--glow-boost) + 10%)), 0 0 1px 0 hsl(var(--glow-color) / calc(var(--glow-boost) + 60%)), 0 0 3px 0 hsl(var(--glow-color) / calc(var(--glow-boost) + 50%)), 0 0 6px 0 hsl(var(--glow-color) / calc(var(--glow-boost) + 40%)), 0 0 15px 0 hsl(var(--glow-color) / calc(var(--glow-boost) + 30%)), 0 0 25px 2px hsl(var(--glow-color) / calc(var(--glow-boost) + 20%)), 0 0 50px 2px hsl(var(--glow-color) / calc(var(--glow-boost) + 10%));
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.card .inner {
|
| 220 |
+
display: flex;
|
| 221 |
+
flex-direction: column;
|
| 222 |
+
justify-content: flex-start;
|
| 223 |
+
position: relative;
|
| 224 |
+
overflow: hidden;
|
| 225 |
+
z-index: 2;
|
| 226 |
+
padding: 24px;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.card header {
|
| 230 |
+
display: flex;
|
| 231 |
+
justify-content: space-between;
|
| 232 |
+
align-items: center;
|
| 233 |
+
margin-bottom: 20px;
|
| 234 |
+
flex-shrink: 0;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.card h2 {
|
| 238 |
+
color: inherit;
|
| 239 |
+
font-weight: 500;
|
| 240 |
+
font-size: 1.25em;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.card .close-button {
|
| 244 |
+
background: none;
|
| 245 |
+
border: none;
|
| 246 |
+
color: white;
|
| 247 |
+
font-size: 24px;
|
| 248 |
+
cursor: pointer;
|
| 249 |
+
opacity: 0.7;
|
| 250 |
+
transition: opacity 0.2s;
|
| 251 |
+
}
|
| 252 |
+
.card .close-button:hover {
|
| 253 |
+
opacity: 1;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.tabs {
|
| 257 |
+
display: flex;
|
| 258 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
| 259 |
+
margin-bottom: 20px;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.tabs button {
|
| 263 |
+
background: none;
|
| 264 |
+
border: none;
|
| 265 |
+
color: rgba(255, 255, 255, 0.6);
|
| 266 |
+
padding: 10px 15px;
|
| 267 |
+
cursor: pointer;
|
| 268 |
+
font-size: 14px;
|
| 269 |
+
border-bottom: 2px solid transparent;
|
| 270 |
+
transition: color 0.2s, border-color 0.2s;
|
| 271 |
+
}
|
| 272 |
+
.tabs button:hover {
|
| 273 |
+
color: white;
|
| 274 |
+
}
|
| 275 |
+
.tabs button.active {
|
| 276 |
+
color: white;
|
| 277 |
+
border-bottom-color: #c299ff;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.panels > .panel {
|
| 281 |
+
display: none;
|
| 282 |
+
}
|
| 283 |
+
.panels > .panel.active {
|
| 284 |
+
display: block;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.panel {
|
| 288 |
+
padding-block: 10px;
|
| 289 |
+
width: 100%;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.panel label {
|
| 293 |
+
display: block;
|
| 294 |
+
margin-bottom: 5px;
|
| 295 |
+
font-size: 14px;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.panel div {
|
| 299 |
+
margin-bottom: 15px;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.panel input[type="range"] {
|
| 303 |
+
width: 100%;
|
| 304 |
+
cursor: pointer;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.panel button {
|
| 308 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 309 |
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
| 310 |
+
color: white;
|
| 311 |
+
padding: 8px 16px;
|
| 312 |
+
font-size: 14px;
|
| 313 |
+
border-radius: 6px;
|
| 314 |
+
cursor: pointer;
|
| 315 |
+
transition: background-color 0.3s;
|
| 316 |
+
}
|
| 317 |
+
.panel button:hover {
|
| 318 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
</style>
|
| 322 |
+
</head>
|
| 323 |
+
<body>
|
| 324 |
+
<!-- O contêiner para a renderização do Three.js -->
|
| 325 |
+
<div id="container"></div>
|
| 326 |
+
<!-- Uma sobreposição para um efeito de brilho sutil -->
|
| 327 |
+
<div class="glow-overlay"></div>
|
| 328 |
+
<!-- Título e Subtítulo -->
|
| 329 |
+
<div class="title-container">
|
| 330 |
+
<h1 class="main-title">Nebulosa Cósmica</h1>
|
| 331 |
+
<p class="subtitle">Uma Experiência Visual Interativa</p>
|
| 332 |
+
</div>
|
| 333 |
+
|
| 334 |
+
<!-- Controles de áudio -->
|
| 335 |
+
<div id="audio-controls">
|
| 336 |
+
<button id="startButton">🎤 Ativar Áudio</button>
|
| 337 |
+
</div>
|
| 338 |
+
<!-- Botão de Configurações -->
|
| 339 |
+
<div id="settings-container">
|
| 340 |
+
<button id="settingsButton" aria-label="Configurações">
|
| 341 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px">
|
| 342 |
+
<path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>
|
| 343 |
+
</svg>
|
| 344 |
+
</button>
|
| 345 |
+
</div>
|
| 346 |
+
|
| 347 |
+
<!-- Popup/Modal de Configurações -->
|
| 348 |
+
<div id="settings-popup" class="popup-overlay">
|
| 349 |
+
<div class="card">
|
| 350 |
+
<span class="glow"></span>
|
| 351 |
+
<div class="inner">
|
| 352 |
+
<header>
|
| 353 |
+
<h2>Configurações</h2>
|
| 354 |
+
<button class="close-button" id="close-popup-button">×</button>
|
| 355 |
+
</header>
|
| 356 |
+
<div class="tabs">
|
| 357 |
+
<button data-tab="visuals-panel" class="active">🎨 Visual</button>
|
| 358 |
+
<button data-tab="audio-panel">🎵 Áudio</button>
|
| 359 |
+
<button data-tab="camera-panel">📷 Câmara</button>
|
| 360 |
+
</div>
|
| 361 |
+
<div class="panels">
|
| 362 |
+
<div id="visuals-panel" class="panel active">
|
| 363 |
+
<div>
|
| 364 |
+
<label for="interactionStrength">🌟 Força da Interação</label>
|
| 365 |
+
<input type="range" id="interactionStrength" min="0" max="0.5" step="0.01" value="0.1">
|
| 366 |
+
</div>
|
| 367 |
+
<div>
|
| 368 |
+
<label for="particleSize">⚡ Tamanho das Partículas</input>
|
| 369 |
+
<input type="range" id="particleSize" min="0.1" max="1.0" step="0.05" value="0.3">
|
| 370 |
+
</div>
|
| 371 |
+
<div>
|
| 372 |
+
<label for="bloomStrength">✨ Intensidade do Brilho</label>
|
| 373 |
+
<input type="range" id="bloomStrength" min="0" max="3" step="0.1" value="1.2">
|
| 374 |
+
</div>
|
| 375 |
+
</div>
|
| 376 |
+
<div id="audio-panel" class="panel">
|
| 377 |
+
<div>
|
| 378 |
+
<label for="audioSensitivity">🎚️ Sensibilidade do Áudio</label>
|
| 379 |
+
<input type="range" id="audioSensitivity" min="0.1" max="2.0" step="0.1" value="1.0">
|
| 380 |
+
</div>
|
| 381 |
+
<div>
|
| 382 |
+
<label for="bassBoost">🔊 Reforço de Graves</label>
|
| 383 |
+
<input type="range" id="bassBoost" min="0.5" max="3.0" step="0.1" value="1.0">
|
| 384 |
+
</div>
|
| 385 |
+
</div>
|
| 386 |
+
<div id="camera-panel" class="panel">
|
| 387 |
+
<div>
|
| 388 |
+
<label for="resetCameraButton">🎯 Controlo da Câmara</label>
|
| 389 |
+
<button id="resetCameraButton">🔄 Repor Posição</button>
|
| 390 |
+
</div>
|
| 391 |
+
<div>
|
| 392 |
+
<label for="cameraSensitivity">🎮 Sensibilidade da Câmara</label>
|
| 393 |
+
<input type="range" id="cameraSensitivity" min="0.1" max="2.0" step="0.1" value="1.0">
|
| 394 |
+
</div>
|
| 395 |
+
</div>
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
<!-- Import Map para gerenciar as dependências do Three.js -->
|
| 403 |
+
<script type="importmap">
|
| 404 |
+
{
|
| 405 |
+
"imports": {
|
| 406 |
+
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
|
| 407 |
+
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
+
</script>
|
| 411 |
+
|
| 412 |
+
<!-- O script principal da aplicação -->
|
| 413 |
+
<script type="module">
|
| 414 |
+
//================================================================================
|
| 415 |
+
// SECÇÃO 1: IMPORTAÇÕES E VARIÁVEIS GLOBAIS
|
| 416 |
+
//================================================================================
|
| 417 |
+
|
| 418 |
+
// Importa os módulos necessários do Three.js
|
| 419 |
+
import * as THREE from 'three';
|
| 420 |
+
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
| 421 |
+
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
| 422 |
+
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
| 423 |
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
| 424 |
+
|
| 425 |
+
// --- Variáveis da Cena Principal ---
|
| 426 |
+
let scene, camera, renderer, composer, controls, bloomPass;
|
| 427 |
+
let particleLayers = []; // Array para armazenar as camadas de partículas
|
| 428 |
+
let time = 0; // Contador de tempo para animações
|
| 429 |
+
let ripples = []; // Array para armazenar os efeitos de ondulação do clique
|
| 430 |
+
const interactionRadius = 40; // Raio de interação da câmara com as partículas
|
| 431 |
+
// --- Variáveis de Configuração (Controladas pelo Painel) ---
|
| 432 |
+
let interactionStrength = 0.1;
|
| 433 |
+
let baseParticleSize = 0.3;
|
| 434 |
+
let audioSensitivity = 1.0;
|
| 435 |
+
let bassBoost = 1.0;
|
| 436 |
+
let cameraSensitivity = 1.0;
|
| 437 |
+
// --- Variáveis de Áudio ---
|
| 438 |
+
let audioContext, analyser, dataArray;
|
| 439 |
+
let audioInitialized = false; // Flag para verificar se o áudio foi iniciado
|
| 440 |
+
// --- Configuração das Camadas de Partículas ---
|
| 441 |
+
// Define as propriedades de cada camada da nebulosa
|
| 442 |
+
const layersConfig = [
|
| 443 |
+
{ count: 20000, baseSize: 0.3, colorRange: { hue: [0.75, 0.9], sat: [0.7, 1], light: [0.5, 0.7] }, rotationSpeed: 0.001 },
|
| 444 |
+
{ count: 25000, baseSize: 0.2, colorRange: { hue: [0.45, 0.6], sat: [0.6, 0.8], light: [0.4, 0.6] }, rotationSpeed: 0.0005 }
|
| 445 |
+
];
|
| 446 |
+
//================================================================================
|
| 447 |
+
// SECÇÃO 2: FUNÇÕES DE CRIAÇÃO E INICIALIZAÇÃO
|
| 448 |
+
//================================================================================
|
| 449 |
+
|
| 450 |
+
/**
|
| 451 |
+
* Cria um sistema de partículas (uma camada da nebulosa).
|
| 452 |
+
* @param {object} config - Objeto de configuração para a camada de partículas.
|
| 453 |
+
* @returns {THREE.Points} - O objeto THREE.Points que representa o sistema de partículas.
|
| 454 |
+
*/
|
| 455 |
+
function createParticleSystem(config) {
|
| 456 |
+
const geometry = new THREE.BufferGeometry();
|
| 457 |
+
const positions = new Float32Array(config.count * 3);
|
| 458 |
+
const colors = new Float32Array(config.count * 3);
|
| 459 |
+
const basePositions = new Float32Array(config.count * 3);
|
| 460 |
+
const baseColors = new Float32Array(config.count * 3);
|
| 461 |
+
|
| 462 |
+
for (let i = 0; i < config.count; i++) {
|
| 463 |
+
const i3 = i * 3;
|
| 464 |
+
// Posiciona as partículas numa distribuição esférica
|
| 465 |
+
const radius = 25 + Math.random() * 30;
|
| 466 |
+
const theta = Math.random() * Math.PI * 2;
|
| 467 |
+
const phi = Math.acos(2 * Math.random() - 1);
|
| 468 |
+
const x = radius * Math.sin(phi) * Math.cos(theta);
|
| 469 |
+
const y = radius * Math.sin(phi) * Math.sin(theta);
|
| 470 |
+
const z = radius * Math.cos(phi);
|
| 471 |
+
positions[i3] = x; positions[i3 + 1] = y; positions[i3 + 2] = z;
|
| 472 |
+
basePositions[i3] = x; basePositions[i3 + 1] = y; basePositions[i3 + 2] = z;
|
| 473 |
+
|
| 474 |
+
// Define a cor com base na distância ao centro
|
| 475 |
+
const dist = Math.sqrt(x * x + y * y + z * z) / 55;
|
| 476 |
+
const hue = THREE.MathUtils.lerp(config.colorRange.hue[0], config.colorRange.hue[1], dist);
|
| 477 |
+
const sat = THREE.MathUtils.lerp(config.colorRange.sat[0], config.colorRange.sat[1], dist);
|
| 478 |
+
const light = THREE.MathUtils.lerp(config.colorRange.light[0], config.colorRange.light[1], dist);
|
| 479 |
+
const color = new THREE.Color().setHSL(hue, sat, light);
|
| 480 |
+
colors[i3] = color.r; colors[i3 + 1] = color.g; colors[i3 + 2] = color.b;
|
| 481 |
+
baseColors[i3] = color.r; baseColors[i3 + 1] = color.g; baseColors[i3 + 2] = color.b;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
| 485 |
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
| 486 |
+
|
| 487 |
+
// Define o material das partículas
|
| 488 |
+
const material = new THREE.PointsMaterial({
|
| 489 |
+
size: config.baseSize,
|
| 490 |
+
vertexColors: true,
|
| 491 |
+
transparent: true,
|
| 492 |
+
opacity: 0.8,
|
| 493 |
+
blending: THREE.AdditiveBlending,
|
| 494 |
+
depthWrite: false,
|
| 495 |
+
sizeAttenuation: true
|
| 496 |
+
});
|
| 497 |
+
const points = new THREE.Points(geometry, material);
|
| 498 |
+
// Armazena dados adicionais para a animação
|
| 499 |
+
points.userData = {
|
| 500 |
+
velocities: new Float32Array(config.count * 3).fill(0),
|
| 501 |
+
basePositions,
|
| 502 |
+
baseColors,
|
| 503 |
+
colorVelocities: new Float32Array(config.count * 3).fill(0),
|
| 504 |
+
rotationSpeed: config.rotationSpeed,
|
| 505 |
+
baseSize: config.baseSize
|
| 506 |
+
};
|
| 507 |
+
return points;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/**
|
| 511 |
+
* Cria um efeito de ondulação (ripple) na posição especificada, usado no clique.
|
| 512 |
+
* @param {number} x - A coordenada x da ondulação.
|
| 513 |
+
* @param {number} y - A coordenada y da ondulação.
|
| 514 |
+
*/
|
| 515 |
+
function createRipple(x, y) {
|
| 516 |
+
ripples.push({ x, y, radius: 0, strength: 2.5, maxRadius: interactionRadius * 4, speed: 4, color: new THREE.Color(0xffffff) });
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
/**
|
| 520 |
+
* Inicializa o Web Audio API para capturar o áudio do microfone.
|
| 521 |
+
*/
|
| 522 |
+
function initAudio() {
|
| 523 |
+
const startButton = document.getElementById('startButton');
|
| 524 |
+
navigator.mediaDevices.getUserMedia({ audio: true })
|
| 525 |
+
.then(function(stream) {
|
| 526 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 527 |
+
analyser = audioContext.createAnalyser();
|
| 528 |
+
const source = audioContext.createMediaStreamSource(stream);
|
| 529 |
+
source.connect(analyser);
|
| 530 |
+
analyser.fftSize = 256;
|
| 531 |
+
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
| 532 |
+
audioInitialized = true;
|
| 533 |
+
startButton.textContent = '🎤 Áudio Ativo';
|
| 534 |
+
startButton.style.backgroundColor = 'rgba(40, 200, 120, 0.3)';
|
| 535 |
+
startButton.disabled = true;
|
| 536 |
+
}).catch(err => {
|
| 537 |
+
console.error('Erro ao acessar o microfone', err);
|
| 538 |
+
startButton.textContent = '❌ Erro de Áudio';
|
| 539 |
+
});
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
/**
|
| 543 |
+
* Função principal de inicialização da cena, controlos e eventos.
|
| 544 |
+
*/
|
| 545 |
+
function init() {
|
| 546 |
+
// Configuração básica da cena Three.js
|
| 547 |
+
const container = document.getElementById('container');
|
| 548 |
+
scene = new THREE.Scene();
|
| 549 |
+
scene.fog = new THREE.FogExp2(0x020108, 0.008);
|
| 550 |
+
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
| 551 |
+
camera.position.z = 100;
|
| 552 |
+
renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 553 |
+
renderer.setPixelRatio(window.devicePixelRatio);
|
| 554 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 555 |
+
renderer.setClearColor(0x020108);
|
| 556 |
+
container.appendChild(renderer.domElement);
|
| 557 |
+
// Configuração dos controlos de órbita
|
| 558 |
+
controls = new OrbitControls(camera, renderer.domElement);
|
| 559 |
+
controls.enableDamping = true;
|
| 560 |
+
controls.dampingFactor = 0.05;
|
| 561 |
+
controls.screenSpacePanning = false;
|
| 562 |
+
controls.minDistance = 20;
|
| 563 |
+
controls.maxDistance = 150;
|
| 564 |
+
// Configuração do pós-processamento (efeito de brilho)
|
| 565 |
+
const renderScene = new RenderPass(scene, camera);
|
| 566 |
+
bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
|
| 567 |
+
bloomPass.threshold = 0;
|
| 568 |
+
bloomPass.strength = 1.2;
|
| 569 |
+
bloomPass.radius = 0.5;
|
| 570 |
+
composer = new EffectComposer(renderer);
|
| 571 |
+
composer.addPass(renderScene);
|
| 572 |
+
composer.addPass(bloomPass);
|
| 573 |
+
|
| 574 |
+
// Cria e adiciona as camadas de partículas à cena
|
| 575 |
+
layersConfig.forEach(config => {
|
| 576 |
+
const particles = createParticleSystem(config);
|
| 577 |
+
particleLayers.push(particles);
|
| 578 |
+
scene.add(particles);
|
| 579 |
+
});
|
| 580 |
+
|
| 581 |
+
// --- Configuração dos Event Listeners ---
|
| 582 |
+
document.getElementById('startButton').addEventListener('click', initAudio);
|
| 583 |
+
document.addEventListener('click', onClick);
|
| 584 |
+
window.addEventListener('resize', onWindowResize);
|
| 585 |
+
|
| 586 |
+
// Lógica do Popup e das Abas
|
| 587 |
+
setupSettingsPanel();
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
/**
|
| 591 |
+
* Configura toda a lógica do painel de configurações, incluindo popup, abas e sliders.
|
| 592 |
+
*/
|
| 593 |
+
function setupSettingsPanel(){
|
| 594 |
+
const settingsButton = document.getElementById('settingsButton');
|
| 595 |
+
const settingsPopup = document.getElementById('settings-popup');
|
| 596 |
+
const closePopupButton = document.getElementById('close-popup-button');
|
| 597 |
+
const card = settingsPopup.querySelector('.card');
|
| 598 |
+
const tabs = settingsPopup.querySelectorAll('.tabs button');
|
| 599 |
+
const panels = settingsPopup.querySelectorAll('.panels .panel');
|
| 600 |
+
|
| 601 |
+
// Abrir e fechar o popup
|
| 602 |
+
settingsButton.addEventListener('click', () => settingsPopup.classList.add('visible'));
|
| 603 |
+
closePopupButton.addEventListener('click', () => settingsPopup.classList.remove('visible'));
|
| 604 |
+
settingsPopup.addEventListener('click', (e) => {
|
| 605 |
+
if (e.target === settingsPopup) settingsPopup.classList.remove('visible');
|
| 606 |
+
});
|
| 607 |
+
|
| 608 |
+
// Lógica de navegação das abas
|
| 609 |
+
tabs.forEach(tab => {
|
| 610 |
+
tab.addEventListener('click', () => {
|
| 611 |
+
tabs.forEach(item => item.classList.remove('active'));
|
| 612 |
+
panels.forEach(panel => panel.classList.remove('active'));
|
| 613 |
+
tab.classList.add('active');
|
| 614 |
+
document.getElementById(tab.dataset.tab).classList.add('active');
|
| 615 |
+
});
|
| 616 |
+
});
|
| 617 |
+
|
| 618 |
+
// Lógica do brilho do cartão (efeito visual)
|
| 619 |
+
card.addEventListener("pointermove", (e) => {
|
| 620 |
+
const rect = card.getBoundingClientRect();
|
| 621 |
+
const x = e.clientX - rect.left, y = e.clientY - rect.top;
|
| 622 |
+
const width = card.offsetWidth, height = card.offsetHeight;
|
| 623 |
+
const px = (x / width) * 100, py = (y / height) * 100;
|
| 624 |
+
const dx = x - width / 2, dy = y - height / 2;
|
| 625 |
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI) + 90;
|
| 626 |
+
let k_x = dx !== 0 ? (width / 2) / Math.abs(dx) : Infinity;
|
| 627 |
+
let k_y = dy !== 0 ? (height / 2) / Math.abs(dy) : Infinity;
|
| 628 |
+
const edge = Math.min(Math.max(1 / Math.min(k_x, k_y), 0), 1) * 100;
|
| 629 |
+
card.style.setProperty('--pointer-°', `${angle.toFixed(3)}deg`);
|
| 630 |
+
card.style.setProperty('--pointer-d', `${edge.toFixed(3)}`);
|
| 631 |
+
});
|
| 632 |
+
|
| 633 |
+
// Listeners dos Sliders e Botões de controlo
|
| 634 |
+
document.getElementById('interactionStrength').addEventListener('input', e => interactionStrength = parseFloat(e.target.value));
|
| 635 |
+
document.getElementById('particleSize').addEventListener('input', e => {
|
| 636 |
+
baseParticleSize = parseFloat(e.target.value);
|
| 637 |
+
particleLayers.forEach(layer => layer.material.size = layer.userData.baseSize * baseParticleSize);
|
| 638 |
+
});
|
| 639 |
+
document.getElementById('bloomStrength').addEventListener('input', e => bloomPass.strength = parseFloat(e.target.value));
|
| 640 |
+
document.getElementById('audioSensitivity').addEventListener('input', e => audioSensitivity = parseFloat(e.target.value));
|
| 641 |
+
document.getElementById('bassBoost').addEventListener('input', e => bassBoost = parseFloat(e.target.value));
|
| 642 |
+
document.getElementById('cameraSensitivity').addEventListener('input', e => {
|
| 643 |
+
cameraSensitivity = parseFloat(e.target.value);
|
| 644 |
+
controls.rotateSpeed = 1.0 * cameraSensitivity;
|
| 645 |
+
controls.panSpeed = 1.0 * cameraSensitivity;
|
| 646 |
+
});
|
| 647 |
+
document.getElementById('resetCameraButton').addEventListener('click', () => {
|
| 648 |
+
controls.reset();
|
| 649 |
+
camera.position.z = 100;
|
| 650 |
+
});
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
//================================================================================
|
| 654 |
+
// SECÇÃO 3: LÓGICA DE ANIMAÇÃO E ATUALIZAÇÃO
|
| 655 |
+
//================================================================================
|
| 656 |
+
|
| 657 |
+
/**
|
| 658 |
+
* Atualiza a posição e cor de cada partícula em cada frame.
|
| 659 |
+
*/
|
| 660 |
+
function updateParticles() {
|
| 661 |
+
let overallVolume = 0, bassLevel = 0;
|
| 662 |
+
// Analisa o áudio se estiver inicializado
|
| 663 |
+
if (audioInitialized) {
|
| 664 |
+
analyser.getByteFrequencyData(dataArray);
|
| 665 |
+
let total = 0;
|
| 666 |
+
for (let i = 0; i < dataArray.length; i++) total += dataArray[i];
|
| 667 |
+
overallVolume = total / dataArray.length;
|
| 668 |
+
let bassTotal = 0;
|
| 669 |
+
const bassEnd = dataArray.length * 0.2; // Frequências graves
|
| 670 |
+
for (let i = 0; i < bassEnd; i++) bassTotal += dataArray[i];
|
| 671 |
+
bassLevel = (bassTotal / bassEnd) / 255;
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
// Atualiza as ondulações ativas
|
| 675 |
+
ripples = ripples.filter(r => (r.radius += r.speed) < r.maxRadius && (r.strength *= 0.96));
|
| 676 |
+
|
| 677 |
+
// Itera sobre cada camada de partículas
|
| 678 |
+
particleLayers.forEach(layer => {
|
| 679 |
+
const { position, color } = layer.geometry.attributes;
|
| 680 |
+
const { velocities, basePositions, baseColors, colorVelocities, baseSize } = layer.userData;
|
| 681 |
+
// Atualiza o tamanho das partículas com base no grave do áudio
|
| 682 |
+
layer.material.size = (baseSize * baseParticleSize) + (bassLevel * 0.5 * audioSensitivity * bassBoost);
|
| 683 |
+
// Itera sobre cada partícula na camada
|
| 684 |
+
for (let i = 0; i < position.count; i++) {
|
| 685 |
+
const i3 = i * 3;
|
| 686 |
+
const currentPos = new THREE.Vector3(position.array[i3], position.array[i3 + 1], position.array[i3 + 2]);
|
| 687 |
+
let totalForce = new THREE.Vector3();
|
| 688 |
+
let colorShift = new THREE.Vector3();
|
| 689 |
+
|
| 690 |
+
// 1. Força de interação com a câmara
|
| 691 |
+
const cameraDist = camera.position.distanceTo(currentPos);
|
| 692 |
+
if (cameraDist < interactionRadius) {
|
| 693 |
+
const forceStrength = (1 - cameraDist / interactionRadius) * interactionStrength;
|
| 694 |
+
const forceDirection = currentPos.clone().sub(camera.position).normalize();
|
| 695 |
+
totalForce.add(forceDirection.multiplyScalar(forceStrength));
|
| 696 |
+
const colorIntensity = (1 - cameraDist / interactionRadius) * 0.8;
|
| 697 |
+
colorShift.set(colorIntensity, colorIntensity, colorIntensity);
|
| 698 |
+
}
|
| 699 |
+
// 2. Força de interação com o áudio
|
| 700 |
+
if (audioInitialized && overallVolume > 1) {
|
| 701 |
+
const audioForceStrength = (overallVolume / 255.0) * 0.4 * audioSensitivity;
|
| 702 |
+
totalForce.add(currentPos.clone().normalize().multiplyScalar(audioForceStrength));
|
| 703 |
+
if (bassLevel > 0.1) colorShift.x += bassLevel * 0.5 * audioSensitivity * bassBoost;
|
| 704 |
+
}
|
| 705 |
+
// 3. Força de interação com as ondulações do clique
|
| 706 |
+
ripples.forEach(ripple => {
|
| 707 |
+
const rippleDist = Math.hypot(ripple.x - currentPos.x, ripple.y - currentPos.y);
|
| 708 |
+
const rippleWidth = 15;
|
| 709 |
+
if (Math.abs(rippleDist - ripple.radius) < rippleWidth) {
|
| 710 |
+
const falloff = 1 - Math.abs(rippleDist - ripple.radius) / rippleWidth;
|
| 711 |
+
const rippleForce = ripple.strength * falloff * 0.1;
|
| 712 |
+
const forceDirection = currentPos.clone().sub(new THREE.Vector3(ripple.x, ripple.y, currentPos.z)).normalize();
|
| 713 |
+
totalForce.add(forceDirection.multiplyScalar(rippleForce));
|
| 714 |
+
colorShift.add(new THREE.Vector3(ripple.color.r, ripple.color.g, ripple.color.b).multiplyScalar(falloff * ripple.strength));
|
| 715 |
+
}
|
| 716 |
+
});
|
| 717 |
+
|
| 718 |
+
// 4. Força de retorno à posição original e amortecimento (damping)
|
| 719 |
+
velocities[i3] += totalForce.x + (basePositions[i3] - currentPos.x) * 0.02;
|
| 720 |
+
velocities[i3 + 1] += totalForce.y + (basePositions[i3 + 1] - currentPos.y) * 0.02;
|
| 721 |
+
velocities[i3 + 2] += totalForce.z + (basePositions[i3 + 2] - currentPos.z) * 0.02;
|
| 722 |
+
velocities[i3] *= 0.94; velocities[i3 + 1] *= 0.94; velocities[i3 + 2] *= 0.94;
|
| 723 |
+
|
| 724 |
+
// Atualiza a posição
|
| 725 |
+
position.array[i3] += velocities[i3];
|
| 726 |
+
position.array[i3 + 1] += velocities[i3 + 1];
|
| 727 |
+
position.array[i3 + 2] += velocities[i3 + 2];
|
| 728 |
+
|
| 729 |
+
// Atualiza a cor (lógica semelhante à da posição)
|
| 730 |
+
colorVelocities[i3] += colorShift.x + (baseColors[i3] - color.array[i3]) * 0.05;
|
| 731 |
+
colorVelocities[i3 + 1] += colorShift.y + (baseColors[i3 + 1] - color.array[i3 + 1]) * 0.05;
|
| 732 |
+
colorVelocities[i3 + 2] += colorShift.z + (baseColors[i3 + 2] - color.array[i3 + 2]) * 0.05;
|
| 733 |
+
colorVelocities[i3] *= 0.9; colorVelocities[i3 + 1] *= 0.9; colorVelocities[i3 + 2] *= 0.9;
|
| 734 |
+
|
| 735 |
+
color.array[i3] += colorVelocities[i3];
|
| 736 |
+
color.array[i3 + 1] += colorVelocities[i3 + 1];
|
| 737 |
+
color.array[i3 + 2] += colorVelocities[i3 + 2];
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
// Informa o Three.js que os atributos foram atualizados
|
| 741 |
+
position.needsUpdate = true;
|
| 742 |
+
color.needsUpdate = true;
|
| 743 |
+
});
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
/**
|
| 747 |
+
* O loop principal de animação, chamado a cada frame.
|
| 748 |
+
*/
|
| 749 |
+
function animate() {
|
| 750 |
+
requestAnimationFrame(animate);
|
| 751 |
+
time += 0.01;
|
| 752 |
+
controls.update(); // Atualiza os controlos de órbita
|
| 753 |
+
updateParticles(); // Atualiza as partículas
|
| 754 |
+
|
| 755 |
+
// Animação de rotação base das camadas
|
| 756 |
+
particleLayers.forEach(layer => {
|
| 757 |
+
layer.rotation.y += layer.userData.rotationSpeed;
|
| 758 |
+
layer.rotation.x = Math.sin(time * 0.1) * 0.05;
|
| 759 |
+
});
|
| 760 |
+
|
| 761 |
+
// Renderiza a cena através do composer para aplicar o pós-processamento
|
| 762 |
+
composer.render();
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
//================================================================================
|
| 766 |
+
// SECÇÃO 4: FUNÇÕES DE EVENTOS DE UTILIZADOR
|
| 767 |
+
//================================================================================
|
| 768 |
+
|
| 769 |
+
/**
|
| 770 |
+
* Adiciona um efeito de ondulação ao clicar no ecrã.
|
| 771 |
+
* @param {MouseEvent} event - O objeto do evento de clique.
|
| 772 |
+
*/
|
| 773 |
+
function onClick(event) {
|
| 774 |
+
// Ignora cliques em botões ou no painel de configurações
|
| 775 |
+
if (event.target.closest('button') || event.target.closest('.card')) return;
|
| 776 |
+
|
| 777 |
+
// Converte as coordenadas do clique do ecrã para coordenadas 3D do mundo
|
| 778 |
+
const clickMouse = new THREE.Vector2((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1);
|
| 779 |
+
const vector = new THREE.Vector3(clickMouse.x, clickMouse.y, 0.5);
|
| 780 |
+
vector.unproject(camera);
|
| 781 |
+
const dir = vector.sub(camera.position).normalize();
|
| 782 |
+
const distance = -camera.position.z / dir.z;
|
| 783 |
+
const pos = camera.position.clone().add(dir.multiplyScalar(distance));
|
| 784 |
+
createRipple(pos.x, pos.y);
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
/**
|
| 788 |
+
* Lida com o redimensionamento da janela do navegador.
|
| 789 |
+
*/
|
| 790 |
+
function onWindowResize() {
|
| 791 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
| 792 |
+
camera.updateProjectionMatrix();
|
| 793 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 794 |
+
composer.setSize(window.innerWidth, window.innerHeight);
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
//================================================================================
|
| 798 |
+
// SECÇÃO 5: INICIALIZAÇÃO DA APLICAÇÃO
|
| 799 |
+
//================================================================================
|
| 800 |
+
init();
|
| 801 |
+
animate();
|
| 802 |
+
</script>
|
| 803 |
+
<script src="https://deepsite.hf.co/deepsite-badge.js"></script>
|
| 804 |
+
</body>
|
| 805 |
</html>
|
| 806 |
+
|
| 807 |
+
|