web-3Dviever-glb/index.html Repository web-3Dviever-glb Original path web-3Dviever-glb/index.htmlRole SOURCE Size 53908 bytes Lines 1890 SHA-256 245ec511830c888022123d3efca5a44eded70f64927cbfaa8ab3645e1463f2beDisplayed range 1–500 Project index · Next file/page
<!doctype html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Scaniverse Model Viewer</title>
<link rel="icon" href="data:," />
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #1b1b1b;
font-family: Arial, sans-serif;
}
canvas {
display: block;
width: 100%;
height: 100%;
outline: none;
touch-action: none;
}
#status {
position: fixed;
top: 16px;
left: 16px;
z-index: 20;
min-width: 230px;
max-width: 470px;
padding: 11px 14px;
color: #ffffff;
background: rgba(0, 0, 0, 0.72);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
font-size: 14px;
line-height: 1.45;
white-space: pre-line;
pointer-events: none;
}
#controls-info {
position: fixed;
left: 16px;
bottom: 16px;
z-index: 20;
padding: 9px 12px;
color: rgba(255, 255, 255, 0.84);
background: rgba(0, 0, 0, 0.58);
border-radius: 7px;
font-size: 12px;
line-height: 1.45;
pointer-events: none;
}
#reset-camera {
position: fixed;
top: 16px;
right: 16px;
z-index: 20;
padding: 9px 13px;
color: #ffffff;
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 7px;
cursor: pointer;
font-size: 13px;
}
#model-picker {
position: fixed;
top: 16px;
right: 140px;
z-index: 20;
display: flex;
gap: 8px;
align-items: center;
padding: 8px;
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 7px;
}
#model-select,
#load-model {
height: 32px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.22);
color: #ffffff;
background: rgba(0, 0, 0, 0.78);
font-size: 13px;
}
#model-select {
min-width: 230px;
padding: 0 8px;
}
#load-model {
padding: 0 12px;
cursor: pointer;
}
#reset-camera:hover,
#load-model:hover {
background: rgba(50, 50, 50, 0.9);
}
#reset-camera:disabled,
#load-model:disabled,
#model-select:disabled,
.mobile-zoom-button:disabled {
opacity: 0.5;
cursor: default;
}
#mobile-zoom,
#mobile-controls-info {
display: none;
}
#mobile-zoom {
position: fixed;
right: max(12px, env(safe-area-inset-right));
bottom: calc(84px + env(safe-area-inset-bottom));
z-index: 24;
flex-direction: column;
gap: 10px;
}
.mobile-zoom-button {
width: 54px;
height: 54px;
padding: 0;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 50%;
color: #ffffff;
background: rgba(0, 0, 0, 0.72);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.3);
font-size: 30px;
font-weight: 300;
line-height: 1;
cursor: pointer;
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
.mobile-zoom-button:active {
transform: scale(0.94);
background: rgba(55, 55, 55, 0.92);
}
#mobile-controls-info {
position: fixed;
left: max(12px, env(safe-area-inset-left));
right: 78px;
bottom: calc(86px + env(safe-area-inset-bottom));
z-index: 20;
max-width: 330px;
padding: 8px 10px;
color: rgba(255, 255, 255, 0.84);
background: rgba(0, 0, 0, 0.58);
border-radius: 7px;
font-size: 11px;
line-height: 1.4;
pointer-events: none;
}
body.mobile-device #mobile-zoom {
display: flex;
}
body.mobile-device #mobile-controls-info {
display: block;
}
@media (max-width: 760px), (pointer: coarse) {
#status {
top: calc(54px + env(safe-area-inset-top));
left: max(10px, env(safe-area-inset-left));
right: max(10px, env(safe-area-inset-right));
min-width: 0;
max-width: none;
padding: 9px 11px;
font-size: 12px;
}
#reset-camera {
top: max(10px, env(safe-area-inset-top));
right: max(10px, env(safe-area-inset-right));
padding: 8px 10px;
font-size: 12px;
}
#model-picker {
top: auto;
left: max(10px, env(safe-area-inset-left));
right: max(10px, env(safe-area-inset-right));
bottom: max(10px, env(safe-area-inset-bottom));
gap: 6px;
padding: 6px;
}
#model-select {
min-width: 0;
flex: 1;
font-size: 12px;
}
#load-model {
padding: 0 10px;
font-size: 12px;
}
#controls-info {
display: none;
}
}
@media (max-width: 430px) {
#mobile-controls-info {
max-width: 230px;
font-size: 10px;
}
.mobile-zoom-button {
width: 50px;
height: 50px;
}
}
</style>
</head>
<body>
<div id="status">Uruchamianie viewera...</div>
<button id="reset-camera" type="button">
Reset kamery
</button>
<div id="model-picker">
<select id="model-select"></select>
<button id="load-model" type="button">Załaduj</button>
</div>
<div id="controls-info">
Lewy przycisk: obrót wokół punktu<br />
Prawy przycisk: przesuwanie<br />
Oba przyciski: rozglądanie się w miejscu<br />
Rolka: przybliżanie<br />
Strzałki: ruch widza<br />
Dwuklik: przelot do miejsca
</div>
<div id="mobile-controls-info">
1 palec: obrót • 2 palce: przesuwanie<br />
Dwukrotne stuknięcie: przelot do miejsca
</div>
<div id="mobile-zoom" aria-label="Przybliżanie modelu">
<button
id="zoom-in"
class="mobile-zoom-button"
type="button"
aria-label="Przybliż"
title="Przybliż"
>+</button>
<button
id="zoom-out"
class="mobile-zoom-button"
type="button"
aria-label="Oddal"
title="Oddal"
>−</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/",
"@sparkjsdev/spark": "https://sparkjs.dev/releases/spark/2.1.0/spark.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { PLYLoader } from "three/addons/loaders/PLYLoader.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { SparkRenderer, SplatMesh } from "@sparkjsdev/spark";
const statusElement = document.getElementById("status");
const resetButton = document.getElementById("reset-camera");
const modelSelect = document.getElementById("model-select");
const loadButton = document.getElementById("load-model");
const zoomInButton = document.getElementById("zoom-in");
const zoomOutButton = document.getElementById("zoom-out");
const PREFER_MATCHING_GLB_WHEN_HIDDEN_BY_OLD_SERVER = true;
const GLB_RENDER_AS_UNLIT_TEXTURE = true;
const KEYBOARD_MOVE_STEP = 0.15;
const DOUBLE_CLICK_FLIGHT_MS = 900;
const IN_PLACE_LOOK_SENSITIVITY = 0.0038;
const MOBILE_ZOOM_FACTOR = 0.84;
const DOUBLE_TAP_MAX_DELAY_MS = 330;
const DOUBLE_TAP_MAX_DISTANCE_PX = 34;
const TAP_MAX_MOVE_PX = 14;
function setStatus(message, isError = false) {
statusElement.textContent = message;
statusElement.style.background = isError
? "rgba(120, 0, 0, 0.86)"
: "rgba(0, 0, 0, 0.72)";
console.log(message);
}
function showStatusOverlay() {
statusElement.style.display = "block";
statusElement.style.opacity = "1";
statusElement.style.transition = "none";
}
function hideStatusOverlay(delay = 3500) {
window.setTimeout(() => {
statusElement.style.transition = "opacity 0.5s";
statusElement.style.opacity = "0";
window.setTimeout(() => {
statusElement.style.display = "none";
}, 550);
}, delay);
}
function setLoadingState(isLoading) {
loadButton.disabled = isLoading;
modelSelect.disabled = isLoading;
resetButton.disabled = isLoading;
zoomInButton.disabled = isLoading;
zoomOutButton.disabled = isLoading;
controls.enabled = !isLoading;
}
const coarsePointerQuery = window.matchMedia("(pointer: coarse)");
function updateMobileMode() {
const isMobile =
coarsePointerQuery.matches ||
(navigator.maxTouchPoints > 0 && window.innerWidth <= 1024);
document.body.classList.toggle("mobile-device", isMobile);
}
updateMobileMode();
coarsePointerQuery.addEventListener?.("change", updateMobileMode);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1b1b1b);
const camera = new THREE.PerspectiveCamera(
55,
window.innerWidth / window.innerHeight,
0.01,
1000
);
camera.up.set(0, 1, 0);
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
powerPreference: "high-performance"
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
const sparkRenderer = new SparkRenderer({ renderer });
scene.add(sparkRenderer);
scene.add(new THREE.HemisphereLight(0xffffff, 0x303030, 2.2));
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.2);
directionalLight.position.set(-3, 5, -4);
scene.add(directionalLight);
/*
* Sterowanie zostało przywrócone do zachowania z poprzedniego HTML:
* te same parametry OrbitControls i stały krok ruchu strzałkami.
*/
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.enableRotate = true;
controls.enablePan = true;
controls.enableZoom = true;
controls.screenSpacePanning = true;
controls.minDistance = 0.05;
controls.maxDistance = 30;
controls.rotateSpeed = 0.55;
controls.panSpeed = 0.7;
controls.zoomSpeed = 0.8;
/*
* Mobile:
* - jeden palec zachowuje obrót OrbitControls,
* - dwa palce wykonują wyłącznie przesuwanie, jak prawy przycisk myszy,
* - zoom realizują widoczne przyciski + i −.
*/
controls.touches.ONE = THREE.TOUCH.ROTATE;
controls.touches.TWO = THREE.TOUCH.PAN;
let homeCameraPosition = new THREE.Vector3(0, 0, -3.2);
let homeCameraTarget = new THREE.Vector3(0, 0, -0.05);
camera.position.copy(homeCameraPosition);
controls.target.copy(homeCameraTarget);
controls.update();
let loadedObject = null;
let loadedKind = null;
let loadedModelRadius = 1;
let loadedModelName = null;
let loadGeneration = 0;
let availableModelNames = new Set();
let flightAnimation = null;
function resetCamera() {
cancelFlight();
camera.position.copy(homeCameraPosition);
camera.up.set(0, 1, 0);
controls.target.copy(homeCameraTarget);
controls.update();
}
resetButton.addEventListener("click", resetCamera);
function zoomCameraByFactor(factor) {
if (!loadedObject || loadButton.disabled) {
return;
}
cancelFlight();
const offset = camera.position.clone().sub(controls.target);
let distance = offset.length();
if (distance < 0.0001) {
camera.getWorldDirection(offset);
offset.multiplyScalar(-1);
distance = 0.0001;
} else {
offset.normalize();
}
const nextDistance = THREE.MathUtils.clamp(
distance * factor,
controls.minDistance,
controls.maxDistance
);
camera.position
.copy(controls.target)
.addScaledVector(offset, nextDistance);
controls.update();
}
let zoomHoldDelay = null;
let zoomHoldInterval = null;
function stopZoomHold() {
if (zoomHoldDelay !== null) {
clearTimeout(zoomHoldDelay);
zoomHoldDelay = null;
}
if (zoomHoldInterval !== null) {
clearInterval(zoomHoldInterval);
zoomHoldInterval = null;
}
}
function bindZoomButton(button, factor) {
button.addEventListener("pointerdown", (event) => {
if (button.disabled) {
return;
}
event.preventDefault();
event.stopPropagation();
stopZoomHold();
zoomCameraByFactor(factor);