Vídeo 360°
Vídeo imersivo gravado em 360° que permite ao aprendiz controlar o ponto de vista durante a reprodução. Cria senso de presença em ambientes reais inacessíveis ou perigosos.
Experimente
Treinamento de astronauta para caminhada espacial (NASA VR/360)
Pontos de interesse
Código-fonte
Dados de exemplo
O título exibido no cabeçalho.
| 1 |
<?php
|
| 2 |
$titulo = 'Explorando uma Sala de Controle';
|
| 3 |
?>
|
Vídeo maior que a janela visível
O <video> não tem o atributo "controls" e é bem mais largo/alto que a janela (overflow: hidden esconde o excesso) — arrastar move a cena por dentro dela, simulando explorar um ambiente maior que a tela.
| 1 |
<div class="pan-demo" data-tipo="video-arrastavel">
|
| 2 |
|
| 3 |
<div class="pan-cabecalho">
|
| 4 |
<p><?= htmlspecialchars($titulo) ?></p>
|
| 5 |
</div>
|
| 6 |
|
| 7 |
<div class="pan-janela">
|
| 8 |
<!-- Troque pelo caminho do seu vídeo -->
|
| 9 |
<video class="pan-elemento" src="seu-video.mp4" preload="metadata"></video>
|
| 10 |
|
| 11 |
<!-- Camada transparente por cima do vídeo: é ela que recebe o arrasto. -->
|
| 12 |
<div class="pan-arrastavel" aria-hidden="true"></div>
|
| 13 |
|
| 14 |
<p class="pan-aviso">Arraste para explorar</p>
|
| 15 |
</div>
|
| 16 |
|
| 17 |
<div class="pan-controles">
|
| 18 |
<button type="button" class="pan-play-pause">Reproduzir</button>
|
| 19 |
</div>
|
| 20 |
|
| 21 |
</div>
|
Card e cabeçalho
Contêiner geral e a caixa de título centralizada.
| 1 |
.pan-demo {
|
| 2 |
max-width: 560px;
|
| 3 |
border: 1px solid #e2e8f0;
|
| 4 |
border-radius: 16px;
|
| 5 |
background: #fff;
|
| 6 |
font-family: sans-serif;
|
| 7 |
overflow: hidden;
|
| 8 |
}
|
| 9 |
|
| 10 |
.pan-cabecalho {
|
| 11 |
padding: 20px 24px;
|
| 12 |
border-bottom: 1px solid #cbd5e1;
|
| 13 |
background: rgba(241, 245, 249, .6);
|
| 14 |
text-align: center;
|
| 15 |
}
|
| 16 |
|
| 17 |
.pan-cabecalho p {
|
| 18 |
margin: 0;
|
| 19 |
font-weight: 700;
|
| 20 |
font-size: 18px;
|
| 21 |
color: #0f172a;
|
| 22 |
}
|
Janela e vídeo arrastável
overflow: hidden esconde o excesso do vídeo, que é bem maior que a janela — só uma parte da cena fica visível por vez.
| 1 |
.pan-janela {
|
| 2 |
position: relative;
|
| 3 |
aspect-ratio: 16 / 9;
|
| 4 |
overflow: hidden;
|
| 5 |
background: #000;
|
| 6 |
}
|
| 7 |
|
| 8 |
.pan-elemento {
|
| 9 |
position: absolute;
|
| 10 |
top: 50%;
|
| 11 |
left: 50%;
|
| 12 |
width: 180%;
|
| 13 |
height: 180%;
|
| 14 |
transform: translate(-50%, -50%);
|
| 15 |
object-fit: cover;
|
| 16 |
pointer-events: none;
|
| 17 |
}
|
| 18 |
|
| 19 |
.pan-arrastavel {
|
| 20 |
position: absolute;
|
| 21 |
inset: 0;
|
| 22 |
cursor: grab;
|
| 23 |
touch-action: none;
|
| 24 |
user-select: none;
|
| 25 |
}
|
| 26 |
|
| 27 |
.pan-arrastavel:active {
|
| 28 |
cursor: grabbing;
|
| 29 |
}
|
| 30 |
|
| 31 |
.pan-aviso {
|
| 32 |
position: absolute;
|
| 33 |
top: 12px;
|
| 34 |
left: 50%;
|
| 35 |
transform: translateX(-50%);
|
| 36 |
margin: 0;
|
| 37 |
padding: 6px 12px;
|
| 38 |
border-radius: 999px;
|
| 39 |
background: rgba(0, 0, 0, .6);
|
| 40 |
color: #fff;
|
| 41 |
font-size: 12px;
|
| 42 |
pointer-events: none;
|
| 43 |
}
|
Controles
Um botão só alterna reproduzir/pausar.
| 1 |
.pan-controles {
|
| 2 |
padding: 12px 16px 16px;
|
| 3 |
text-align: center;
|
| 4 |
}
|
| 5 |
|
| 6 |
.pan-play-pause {
|
| 7 |
padding: 10px 24px;
|
| 8 |
border: none;
|
| 9 |
border-radius: 999px;
|
| 10 |
background: #6366f1;
|
| 11 |
color: #fff;
|
| 12 |
font: inherit;
|
| 13 |
font-weight: 600;
|
| 14 |
cursor: pointer;
|
| 15 |
}
|
Arrastar sem confundir com um clique
A mesma camada transparente recebe os dois gestos — um clique sem deslocamento alterna reproduzir/pausar; só um arrasto de verdade move a cena. Sem essa distinção, todo clique seria interpretado como os dois ao mesmo tempo.
| 1 |
document.addEventListener('DOMContentLoaded', function () {
|
| 2 |
var demo = document.querySelector('[data-tipo="video-arrastavel"]');
|
| 3 |
var video = demo.querySelector('.pan-elemento');
|
| 4 |
var janela = demo.querySelector('.pan-janela');
|
| 5 |
var arrastavel = demo.querySelector('.pan-arrastavel');
|
| 6 |
var playPauseBtn = demo.querySelector('.pan-play-pause');
|
| 7 |
|
| 8 |
function alternarPlayPause() {
|
| 9 |
if (video.paused) { video.play(); playPauseBtn.textContent = 'Pausar'; }
|
| 10 |
else { video.pause(); playPauseBtn.textContent = 'Reproduzir'; }
|
| 11 |
}
|
| 12 |
|
| 13 |
playPauseBtn.addEventListener('click', alternarPlayPause);
|
Mover a cena na mesma direção do arrasto
O conteúdo acompanha o mouse, como arrastar uma foto — arrastar pra direita revela o que estava escondido à esquerda. deslocX/deslocY guardam a posição entre um arrasto e o próximo.
| 1 |
var arrastando = false;
|
| 2 |
var moveu = false;
|
| 3 |
var inicioX = 0, inicioY = 0;
|
| 4 |
var deslocX = 0, deslocY = 0;
|
| 5 |
var LIMIAR = 4; // pixels — abaixo disso ainda conta como clique, não arrasto
|
| 6 |
|
| 7 |
function limites() {
|
| 8 |
return {
|
| 9 |
x: Math.max(0, (video.offsetWidth - janela.clientWidth) / 2),
|
| 10 |
y: Math.max(0, (video.offsetHeight - janela.clientHeight) / 2)
|
| 11 |
};
|
| 12 |
}
|
| 13 |
|
| 14 |
function aplicarPosicao(x, y) {
|
| 15 |
video.style.transform = 'translate(calc(-50% + ' + x + 'px), calc(-50% + ' + y + 'px))';
|
| 16 |
}
|
| 17 |
|
| 18 |
arrastavel.addEventListener('pointerdown', function (evento) {
|
| 19 |
arrastando = true;
|
| 20 |
moveu = false;
|
| 21 |
inicioX = evento.clientX;
|
| 22 |
inicioY = evento.clientY;
|
| 23 |
arrastavel.setPointerCapture(evento.pointerId);
|
| 24 |
});
|
| 25 |
|
| 26 |
arrastavel.addEventListener('pointermove', function (evento) {
|
| 27 |
if (!arrastando) return;
|
| 28 |
var dx = evento.clientX - inicioX;
|
| 29 |
var dy = evento.clientY - inicioY;
|
| 30 |
if (!moveu && Math.abs(dx) + Math.abs(dy) > LIMIAR) moveu = true;
|
| 31 |
if (!moveu) return;
|
| 32 |
|
| 33 |
var lim = limites();
|
| 34 |
aplicarPosicao(
|
| 35 |
Math.max(-lim.x, Math.min(lim.x, deslocX + dx)),
|
| 36 |
Math.max(-lim.y, Math.min(lim.y, deslocY + dy))
|
| 37 |
);
|
| 38 |
});
|
| 39 |
|
| 40 |
function soltar(evento) {
|
| 41 |
if (!arrastando) return;
|
| 42 |
arrastando = false;
|
| 43 |
if (arrastavel.hasPointerCapture && arrastavel.hasPointerCapture(evento.pointerId)) {
|
| 44 |
arrastavel.releasePointerCapture(evento.pointerId);
|
| 45 |
}
|
| 46 |
if (moveu) {
|
| 47 |
var lim = limites();
|
| 48 |
deslocX = Math.max(-lim.x, Math.min(lim.x, deslocX + (evento.clientX - inicioX)));
|
| 49 |
deslocY = Math.max(-lim.y, Math.min(lim.y, deslocY + (evento.clientY - inicioY)));
|
| 50 |
} else {
|
| 51 |
alternarPlayPause(); // foi um clique de verdade, não um arrasto
|
| 52 |
}
|
| 53 |
}
|
| 54 |
arrastavel.addEventListener('pointerup', soltar);
|
| 55 |
arrastavel.addEventListener('pointercancel', function () { arrastando = false; });
|