738 lines
35 KiB
HTML
738 lines
35 KiB
HTML
<!--
|
|
/* @license
|
|
* Copyright 2020 Google Inc. All Rights Reserved.
|
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
-->
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<title><model-viewer> Annotations</title>
|
|
<meta charset="utf-8">
|
|
<meta name="description" content="<model-viewer> annotation examples">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link type="text/css" href="../../styles/examples.css" rel="stylesheet" />
|
|
<link type="text/css" href="../../styles/docs.css" rel="stylesheet" />
|
|
<link rel="shortcut icon" type="image/png" href="../../assets/favicon.png" />
|
|
|
|
|
|
|
|
<script defer src="https://web3dsurvey.com/collector.js"></script>
|
|
<script>
|
|
window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date;
|
|
ga('create', 'UA-169901325-1', { 'storage': 'none' });
|
|
ga('set', 'referrer', document.referrer.split('?')[0]);
|
|
ga('set', 'anonymizeIp', true);
|
|
ga('send', 'pageview');
|
|
</script>
|
|
<script async src='https://www.google-analytics.com/analytics.js'></script>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="examples-page">
|
|
<div class="sidebar" id="sidenav"></div>
|
|
<div id="toggle"></div>
|
|
<div class="examples-container">
|
|
<div class="sample">
|
|
<div id="addHotspots" class="demo"></div>
|
|
<div class="content">
|
|
<div class="wrapper">
|
|
<h4><span class="font-medium">Annotations. </span></h4>
|
|
<p>
|
|
Use <model-viewer> to make your models interactive.
|
|
This page showcases how you can add hotspots to your scene. Child elements are hotspots if their slot
|
|
begins with "hotspot".
|
|
The data-position attribute specifies the 3D position of the hotspot in model coordinates, using the same
|
|
format as the
|
|
camera-target attribute. The data-normal attribute specifies the normal vector defining the "front" of the
|
|
hotspot.
|
|
When this normal is pointed away from the viewer, the hotspot's opacity becomes --min-hotspot-opacity. The
|
|
<a href="../../editor">Editor</a> lets you drag and drop your own models and add hotspots by clicking
|
|
and displays the corresponding position and normal attributes which you can copy into your own page.
|
|
</p>
|
|
<p>
|
|
You can also specify an attribute to be toggled when the hotspot changes between hidden and visible with
|
|
data-visibility-attribute.
|
|
For example, if you set data-visibility-attribute="visible", then <model-viewer> will toggle the
|
|
data-visible attribute on
|
|
that hotspot element. This makes it easier to write CSS that styles a hotspot based on its visibility.
|
|
</p>
|
|
<p>
|
|
When a hotspot's visibility changes, it will dispatch a "hotspot-visibility" event, and
|
|
event.detail.visible will be true or false
|
|
depending on whether the hotspot is facing the camera.
|
|
</p>
|
|
<div class="heading">
|
|
<h2 class="demo-title">Add hotspots</h2>
|
|
<h4></h4>
|
|
</div>
|
|
<example-snippet stamp-to="addHotspots" highlight-as="html">
|
|
<template>
|
|
<style>
|
|
.hotspot {
|
|
display: block;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 10px;
|
|
border: none;
|
|
background-color: blue;
|
|
box-sizing: border-box;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hotspot[slot="hotspot-hand"] {
|
|
--min-hotspot-opacity: 0;
|
|
background-color: red;
|
|
}
|
|
|
|
.hotspot[slot="hotspot-foot"]:not([data-visible]) {
|
|
background-color: transparent;
|
|
border: 3px solid blue;
|
|
}
|
|
|
|
.annotation {
|
|
background-color: #888888;
|
|
position: absolute;
|
|
transform: translate(10px, 10px);
|
|
border-radius: 10px;
|
|
padding: 10px;
|
|
}
|
|
|
|
/* This keeps child nodes hidden while the element loads */
|
|
:not(:defined)>* {
|
|
display: none;
|
|
}
|
|
</style>
|
|
<model-viewer id="hotspot-demo" ar ar-modes="webxr" camera-controls touch-action="pan-y"
|
|
src="../../shared-assets/models/Astronaut.glb" poster="../../assets/poster-astronaut.webp"
|
|
tone-mapping="aces" shadow-intensity="1" alt="A 3D model of an astronaut.">
|
|
<button class="hotspot" slot="hotspot-visor" data-position="0 1.75 0.35" data-normal="0 0 1"></button>
|
|
<button class="hotspot" slot="hotspot-hand" data-position="-0.54 0.93 0.1"
|
|
data-normal="-0.73 0.05 0.69">
|
|
<div class="annotation">This hotspot disappears completely</div>
|
|
</button>
|
|
<button class="hotspot" slot="hotspot-foot" data-position="0.16 0.1 0.17"
|
|
data-normal="-0.07 0.97 0.23" data-visibility-attribute="visible"></button>
|
|
<div class="annotation">This annotation is fixed in screen-space</div>
|
|
</model-viewer>
|
|
</template>
|
|
</example-snippet>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sample">
|
|
<div id="animatedHotspots" class="demo"></div>
|
|
<div class="content">
|
|
<div class="wrapper">
|
|
<h4 id="intro"><span class="font-medium">Animated Hotspots. </span></h4>
|
|
<p>
|
|
Hotspots can follow a point on the surface of an animated model by
|
|
specifying the data-surface attribute instead of world-space
|
|
position and normal. You can use the <a
|
|
href="../../docs/index.html#entrydocs-annotations-methods-surfaceFromPoint">surfaceFromPoint</a>
|
|
method to get this data-surface string, or even easier, just load
|
|
your model in our <a href="https://modelviewer.dev/editor/">editor</a> and add the
|
|
hotspots there.
|
|
</p>
|
|
<p>
|
|
Hotspots can be queried to build custom overlays in screen space.
|
|
For example, by using an SVG canvas and updating the line nodes we
|
|
can draw label standoff lines.
|
|
</p>
|
|
<p>
|
|
By using WebXR mode, this markup works equally well in AR. Note that
|
|
because the text is not part of the 3D rendering it is easier to
|
|
read and more accessible.
|
|
</p>
|
|
<example-snippet stamp-to="animatedHotspots" highlight-as="html">
|
|
<template>
|
|
<model-viewer id="animation-demo" autoplay ar ar-modes="webxr" scale="0.01 0.01 0.01"
|
|
camera-orbit="90deg auto auto" shadow-intensity="1" camera-controls touch-action="pan-y"
|
|
src="../../shared-assets/models/Horse.glb" alt="A 3D model of a horse galloping.">
|
|
<div slot="hotspot-nose" class="anchor" data-surface="0 0 228 113 111 0.217 0.341 0.442"></div>
|
|
<div slot="hotspot-hoof" class="anchor" data-surface="0 0 752 733 735 0.132 0.379 0.489"></div>
|
|
<div slot="hotspot-tail" class="anchor" data-surface="0 0 220 221 222 0.405 0.061 0.534"></div>
|
|
<svg id="lines" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" class="lineContainer">
|
|
<line class="line"></line>
|
|
<line class="line"></line>
|
|
<line class="line"></line>
|
|
</svg>
|
|
|
|
<div id="container">
|
|
<button id="nose" class="label">Nose</button>
|
|
<button id="hoof" class="label">Hoof</button>
|
|
<button id="tail" class="label">Tail</button>
|
|
</div>
|
|
</model-viewer>
|
|
|
|
<script type="module">
|
|
const modelViewer1 = document.querySelector('#animation-demo');
|
|
const lines = modelViewer1.querySelectorAll('line');
|
|
let baseRect;
|
|
let noseRect;
|
|
let hoofRect;
|
|
let tailRect;
|
|
|
|
function onResize() {
|
|
const arStatus = modelViewer1.getAttribute('ar-status');
|
|
baseRect = (arStatus == "not-presenting" || arStatus == "failed") ?
|
|
modelViewer1.getBoundingClientRect() : new DOMRect();
|
|
noseRect = document.querySelector('#nose').getBoundingClientRect();
|
|
hoofRect = document.querySelector('#hoof').getBoundingClientRect();
|
|
tailRect = document.querySelector('#tail').getBoundingClientRect();
|
|
}
|
|
|
|
window.addEventListener("resize", onResize);
|
|
|
|
modelViewer1.addEventListener('ar-status', (event) => {
|
|
lines.forEach((element) => {
|
|
if (event.detail.status !== 'session-started') {
|
|
element.classList.remove('hide');
|
|
} else {
|
|
element.classList.add('hide');
|
|
}
|
|
});
|
|
onResize();
|
|
});
|
|
|
|
modelViewer1.addEventListener('load', () => {
|
|
onResize();
|
|
// update svg
|
|
function drawLine(svgLine, name, rect) {
|
|
const hotspot = modelViewer1.queryHotspot('hotspot-' + name);
|
|
svgLine.setAttribute('x1', hotspot.canvasPosition.x);
|
|
svgLine.setAttribute('y1', hotspot.canvasPosition.y);
|
|
svgLine.setAttribute('x2', (rect.left + rect.right) / 2 - baseRect.left);
|
|
svgLine.setAttribute('y2', rect.top - baseRect.top);
|
|
}
|
|
|
|
// use requestAnimationFrame to update with renderer
|
|
const startSVGRenderLoop = () => {
|
|
drawLine(lines[0], 'nose', noseRect);
|
|
drawLine(lines[1], 'hoof', hoofRect);
|
|
drawLine(lines[2], 'tail', tailRect);
|
|
requestAnimationFrame(startSVGRenderLoop);
|
|
};
|
|
|
|
startSVGRenderLoop();
|
|
});
|
|
</script>
|
|
<style>
|
|
.anchor {
|
|
display: none;
|
|
}
|
|
|
|
.lineContainer {
|
|
pointer-events: none;
|
|
display: block;
|
|
}
|
|
|
|
.line {
|
|
stroke: #16a5e6;
|
|
stroke-width: 2;
|
|
stroke-dasharray: 2
|
|
}
|
|
|
|
#container {
|
|
position: absolute;
|
|
display: flex;
|
|
justify-content: space-evenly;
|
|
bottom: 8px;
|
|
left: 8px;
|
|
width: 100%;
|
|
}
|
|
|
|
.label {
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
border: none;
|
|
box-sizing: border-box;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
|
|
color: rgba(0, 0, 0, 0.8);
|
|
display: block;
|
|
font-family: Futura, Helvetica Neue, sans-serif;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
max-width: 128px;
|
|
padding: 0.5em 1em;
|
|
bottom: 10px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#animation-demo::part(default-ar-button) {
|
|
bottom: 64px;
|
|
}
|
|
|
|
/* This keeps child nodes hidden while the element loads */
|
|
:not(:defined)>* {
|
|
display: none;
|
|
}
|
|
</style>
|
|
</template>
|
|
</example-snippet>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sample">
|
|
<div id="dimensions" class="demo"></div>
|
|
<div class="content">
|
|
<div class="wrapper">
|
|
<h4><span class="font-medium">Show Dimensions. </span></h4>
|
|
<p>
|
|
Hotspots don't have to be on the model; they can be at arbitrary
|
|
points in 3D space. Here we demonstrate using hotspots to show a
|
|
model's dimensions, entirely styled with CSS. A JavaScript function
|
|
is used to automatically set the dimension values by querying the
|
|
<model-viewer> element.
|
|
</p>
|
|
<p>
|
|
Hotspots can be queried to build custom overlays in screen space.
|
|
For example, by using an SVG canvas and updating the line nodes we
|
|
can draw rudimentary dimension lines. Note that this kind of
|
|
extended functionality cannot support occlusion in the 3D scene, as
|
|
it always draws on top of the model.
|
|
</p>
|
|
<p>
|
|
By using WebXR mode, this markup works equally well in AR. Note that
|
|
because the text is not part of the 3D rendering it is easier to
|
|
read and more accessible.
|
|
</p>
|
|
<example-snippet stamp-to="dimensions" highlight-as="html">
|
|
<template>
|
|
<model-viewer id="dimension-demo" ar ar-modes="webxr" ar-scale="fixed" camera-orbit="-30deg auto auto"
|
|
max-camera-orbit="auto 100deg auto" shadow-intensity="1" camera-controls touch-action="pan-y"
|
|
src="../../assets/ShopifyModels/Chair.glb" alt="A 3D model of an armchair.">
|
|
<button slot="hotspot-dot+X-Y+Z" class="dot" data-position="1 -1 1" data-normal="1 0 0"></button>
|
|
<button slot="hotspot-dim+X-Y" class="dim" data-position="1 -1 0" data-normal="1 0 0"></button>
|
|
<button slot="hotspot-dot+X-Y-Z" class="dot" data-position="1 -1 -1" data-normal="1 0 0"></button>
|
|
<button slot="hotspot-dim+X-Z" class="dim" data-position="1 0 -1" data-normal="1 0 0"></button>
|
|
<button slot="hotspot-dot+X+Y-Z" class="dot" data-position="1 1 -1" data-normal="0 1 0"></button>
|
|
<button slot="hotspot-dim+Y-Z" class="dim" data-position="0 -1 -1" data-normal="0 1 0"></button>
|
|
<button slot="hotspot-dot-X+Y-Z" class="dot" data-position="-1 1 -1" data-normal="0 1 0"></button>
|
|
<button slot="hotspot-dim-X-Z" class="dim" data-position="-1 0 -1" data-normal="-1 0 0"></button>
|
|
<button slot="hotspot-dot-X-Y-Z" class="dot" data-position="-1 -1 -1" data-normal="-1 0 0"></button>
|
|
<button slot="hotspot-dim-X-Y" class="dim" data-position="-1 -1 0" data-normal="-1 0 0"></button>
|
|
<button slot="hotspot-dot-X-Y+Z" class="dot" data-position="-1 -1 1" data-normal="-1 0 0"></button>
|
|
<svg id="dimLines" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"
|
|
class="dimensionLineContainer">
|
|
<line class="dimensionLine"></line>
|
|
<line class="dimensionLine"></line>
|
|
<line class="dimensionLine"></line>
|
|
<line class="dimensionLine"></line>
|
|
<line class="dimensionLine"></line>
|
|
</svg>
|
|
|
|
<div id="controls" class="dim glass">
|
|
<label for="src">Product:</label>
|
|
<select id="src">
|
|
<option value="../../assets/ShopifyModels/Chair.glb">Chair</option>
|
|
<option value="../../assets/ShopifyModels/Mixer.glb">Mixer</option>
|
|
<option value="../../assets/ShopifyModels/GeoPlanter.glb">Cactus</option>
|
|
</select><br />
|
|
|
|
<label for="show-dimensions">Show Dimensions:</label>
|
|
<input id="show-dimensions" type="checkbox" checked="true">
|
|
</div>
|
|
</model-viewer>
|
|
|
|
<script type="module">
|
|
const modelViewer = document.querySelector('#dimension-demo');
|
|
|
|
modelViewer.querySelector('#src').addEventListener('input', (event) => {
|
|
modelViewer.src = event.target.value;
|
|
});
|
|
|
|
const checkbox = modelViewer.querySelector('#show-dimensions');
|
|
|
|
const dimElements = [...modelViewer.querySelectorAll('button'), modelViewer.querySelector('#dimLines')];
|
|
|
|
function setVisibility(visible) {
|
|
dimElements.forEach((element) => {
|
|
if (visible) {
|
|
element.classList.remove('hide');
|
|
} else {
|
|
element.classList.add('hide');
|
|
}
|
|
});
|
|
}
|
|
|
|
checkbox.addEventListener('change', () => {
|
|
setVisibility(checkbox.checked);
|
|
});
|
|
|
|
modelViewer.addEventListener('ar-status', (event) => {
|
|
setVisibility(checkbox.checked && event.detail.status !== 'session-started');
|
|
});
|
|
|
|
// update svg
|
|
function drawLine(svgLine, dotHotspot1, dotHotspot2, dimensionHotspot) {
|
|
if (dotHotspot1 && dotHotspot2) {
|
|
svgLine.setAttribute('x1', dotHotspot1.canvasPosition.x);
|
|
svgLine.setAttribute('y1', dotHotspot1.canvasPosition.y);
|
|
svgLine.setAttribute('x2', dotHotspot2.canvasPosition.x);
|
|
svgLine.setAttribute('y2', dotHotspot2.canvasPosition.y);
|
|
|
|
// use provided optional hotspot to tie visibility of this svg line to
|
|
if (dimensionHotspot && !dimensionHotspot.facingCamera) {
|
|
svgLine.classList.add('hide');
|
|
}
|
|
else {
|
|
svgLine.classList.remove('hide');
|
|
}
|
|
}
|
|
}
|
|
|
|
const dimLines = modelViewer.querySelectorAll('line');
|
|
|
|
const renderSVG = () => {
|
|
drawLine(dimLines[0], modelViewer.queryHotspot('hotspot-dot+X-Y+Z'), modelViewer.queryHotspot('hotspot-dot+X-Y-Z'), modelViewer.queryHotspot('hotspot-dim+X-Y'));
|
|
drawLine(dimLines[1], modelViewer.queryHotspot('hotspot-dot+X-Y-Z'), modelViewer.queryHotspot('hotspot-dot+X+Y-Z'), modelViewer.queryHotspot('hotspot-dim+X-Z'));
|
|
drawLine(dimLines[2], modelViewer.queryHotspot('hotspot-dot+X+Y-Z'), modelViewer.queryHotspot('hotspot-dot-X+Y-Z')); // always visible
|
|
drawLine(dimLines[3], modelViewer.queryHotspot('hotspot-dot-X+Y-Z'), modelViewer.queryHotspot('hotspot-dot-X-Y-Z'), modelViewer.queryHotspot('hotspot-dim-X-Z'));
|
|
drawLine(dimLines[4], modelViewer.queryHotspot('hotspot-dot-X-Y-Z'), modelViewer.queryHotspot('hotspot-dot-X-Y+Z'), modelViewer.queryHotspot('hotspot-dim-X-Y'));
|
|
};
|
|
|
|
modelViewer.addEventListener('load', () => {
|
|
const center = modelViewer.getBoundingBoxCenter();
|
|
const size = modelViewer.getDimensions();
|
|
const x2 = size.x / 2;
|
|
const y2 = size.y / 2;
|
|
const z2 = size.z / 2;
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dot+X-Y+Z',
|
|
position: `${center.x + x2} ${center.y - y2} ${center.z + z2}`
|
|
});
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dim+X-Y',
|
|
position: `${center.x + x2 * 1.2} ${center.y - y2 * 1.1} ${center.z}`
|
|
});
|
|
modelViewer.querySelector('button[slot="hotspot-dim+X-Y"]').textContent =
|
|
`${(size.z * 100).toFixed(0)} cm`;
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dot+X-Y-Z',
|
|
position: `${center.x + x2} ${center.y - y2} ${center.z - z2}`
|
|
});
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dim+X-Z',
|
|
position: `${center.x + x2 * 1.2} ${center.y} ${center.z - z2 * 1.2}`
|
|
});
|
|
modelViewer.querySelector('button[slot="hotspot-dim+X-Z"]').textContent =
|
|
`${(size.y * 100).toFixed(0)} cm`;
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dot+X+Y-Z',
|
|
position: `${center.x + x2} ${center.y + y2} ${center.z - z2}`
|
|
});
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dim+Y-Z',
|
|
position: `${center.x} ${center.y + y2 * 1.1} ${center.z - z2 * 1.1}`
|
|
});
|
|
modelViewer.querySelector('button[slot="hotspot-dim+Y-Z"]').textContent =
|
|
`${(size.x * 100).toFixed(0)} cm`;
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dot-X+Y-Z',
|
|
position: `${center.x - x2} ${center.y + y2} ${center.z - z2}`
|
|
});
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dim-X-Z',
|
|
position: `${center.x - x2 * 1.2} ${center.y} ${center.z - z2 * 1.2}`
|
|
});
|
|
modelViewer.querySelector('button[slot="hotspot-dim-X-Z"]').textContent =
|
|
`${(size.y * 100).toFixed(0)} cm`;
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dot-X-Y-Z',
|
|
position: `${center.x - x2} ${center.y - y2} ${center.z - z2}`
|
|
});
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dim-X-Y',
|
|
position: `${center.x - x2 * 1.2} ${center.y - y2 * 1.1} ${center.z}`
|
|
});
|
|
modelViewer.querySelector('button[slot="hotspot-dim-X-Y"]').textContent =
|
|
`${(size.z * 100).toFixed(0)} cm`;
|
|
|
|
modelViewer.updateHotspot({
|
|
name: 'hotspot-dot-X-Y+Z',
|
|
position: `${center.x - x2} ${center.y - y2} ${center.z + z2}`
|
|
});
|
|
|
|
renderSVG();
|
|
|
|
modelViewer.addEventListener('camera-change', renderSVG);
|
|
});
|
|
</script>
|
|
<style>
|
|
#controls {
|
|
position: absolute;
|
|
bottom: 16px;
|
|
left: 16px;
|
|
max-width: unset;
|
|
transform: unset;
|
|
pointer-events: auto;
|
|
z-index: 100;
|
|
}
|
|
|
|
.dot {
|
|
display: none;
|
|
}
|
|
|
|
.glass {
|
|
background: rgba(255, 255, 255, 0.37);
|
|
backdrop-filter: blur(8px) contrast(0.89) saturate(1.27);
|
|
-webkit-backdrop-filter: blur(8px) contrast(0.89) saturate(1.27);
|
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
padding: 0.5rem;
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.dim {
|
|
border-radius: 4px;
|
|
border: none;
|
|
box-sizing: border-box;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
|
|
color: rgba(0, 0, 0, 0.8);
|
|
display: block;
|
|
font-family: Futura, Helvetica Neue, sans-serif;
|
|
font-size: 1em;
|
|
font-weight: 700;
|
|
max-width: 128px;
|
|
overflow-wrap: break-word;
|
|
padding: 0.5em 1em;
|
|
position: absolute;
|
|
width: max-content;
|
|
height: max-content;
|
|
transform: translate3d(-50%, -50%, 0);
|
|
pointer-events: none;
|
|
--min-hotspot-opacity: 0;
|
|
}
|
|
|
|
@media only screen and (max-width: 800px) {
|
|
.dim {
|
|
font-size: 3vw;
|
|
}
|
|
}
|
|
|
|
.dimensionLineContainer {
|
|
pointer-events: none;
|
|
display: block;
|
|
}
|
|
|
|
.dimensionLine {
|
|
stroke: #16a5e6;
|
|
stroke-width: 2;
|
|
stroke-dasharray: 2;
|
|
}
|
|
|
|
.hide {
|
|
display: none;
|
|
}
|
|
|
|
/* This keeps child nodes hidden while the element loads */
|
|
:not(:defined)>* {
|
|
display: none;
|
|
}
|
|
</style>
|
|
</template>
|
|
</example-snippet>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sample">
|
|
<div id="cameraViews" class="demo"></div>
|
|
<div class="content">
|
|
<div class="wrapper">
|
|
<h4><span class="font-medium">Camera Views. </span></h4>
|
|
<p>
|
|
Hotspots can contain additional data attributes and can be interactive. This example attaches click events
|
|
to hotspots to move the camera.
|
|
</p>
|
|
<p>
|
|
Showcase by <a href="https://twitter.com/hybridherbst">hybridherbst</a> / <a
|
|
href="https://prefrontalcortex.de">prefrontal cortex</a>.<br />
|
|
</p>
|
|
<example-snippet stamp-to="cameraViews" highlight-as="html">
|
|
<template>
|
|
|
|
<model-viewer id="hotspot-camera-view-demo"
|
|
src="../../assets/SketchfabModels/ThorAndTheMidgardSerpent.glb" alt="Thor and the Midgard Serpent"
|
|
camera-controls touch-action="none" camera-orbit="-8.142746deg 68.967deg 0.6179899m"
|
|
camera-target="-0.003m 0.0722m 0.0391m" field-of-view="45deg" min-field-of-view="25deg"
|
|
max-field-of-view="45deg" interpolation-decay="200" min-camera-orbit="auto auto 5%"
|
|
touch-action="none" poster="../../assets/SketchfabModels/ThorAndTheMidgardSerpent.webp"
|
|
tone-mapping="aces" ar>
|
|
<button class="view-button" slot="hotspot-0" data-position="-0.0569m 0.0969m -0.1398m"
|
|
data-normal="-0.5829775m 0.2863482m -0.7603565m" data-orbit="-50.94862deg 84.56856deg 0.06545582m"
|
|
data-target="-0.04384604m 0.07348397m -0.1213202m">
|
|
The Fighters
|
|
</button>
|
|
<button class="view-button" slot="hotspot-1" data-position="-0.1997m 0.11766m 0.0056m"
|
|
data-normal="-0.4421014m 0.04410423m 0.8958802m" data-orbit="3.711166deg 92.3035deg 0.04335197m"
|
|
data-target="-0.1879433m 0.1157161m -0.01563221m">
|
|
Hold Tight!
|
|
</button>
|
|
<button class="view-button" slot="hotspot-2" data-position="0.0608m 0.0566m 0.0605m"
|
|
data-normal="0.2040984m 0.7985359m -0.56629m" data-orbit="42.72974deg 84.74043deg 0.07104211m"
|
|
data-target="0.0757959m 0.04128428m 0.07109568m">
|
|
The Encounter
|
|
</button>
|
|
<button class="view-button" slot="hotspot-3" data-position="0.1989m 0.16711m -0.0749m"
|
|
data-normal="0.7045857m 0.1997957m -0.6809117m" data-orbit="-40.11996deg 88.17818deg 0.07090651m"
|
|
data-target="0.2011831m 0.1398312m -0.07917573m">
|
|
Catapult
|
|
</button>
|
|
<button class="view-button" slot="hotspot-4" data-position="0.0677m 0.18906m -0.0158m"
|
|
data-normal="-0.008245394m 0.6207898m 0.7839338m" data-orbit="-118.8446deg 98.83521deg 0.06m"
|
|
data-target="0.06528695m 0.1753406m -0.01964653m">
|
|
Thunder and Lightning
|
|
</button>
|
|
<button class="view-button" slot="hotspot-5" data-position="-0.1418m -0.041m 0.174m"
|
|
data-normal="-0.4924125m 0.4698265m 0.7326617m" data-orbit="-2.305313deg 110.1798deg 0.04504082m"
|
|
data-target="-0.1151219m -0.04192762m 0.1523764m">
|
|
Knock Knock
|
|
</button>
|
|
<button class="view-button" slot="hotspot-6" data-position="0.08414419m 0.134m -0.215m"
|
|
data-normal="0.03777227m 0.06876653m -0.9969176m" data-orbit="-37.54149deg 82.16209deg 0.0468692m"
|
|
data-target="0.08566038m 0.1249514m -0.1939646m">
|
|
Lucky Shot
|
|
</button>
|
|
<button class="view-button" slot="hotspot-7" data-position="0.14598m 0.03177m -0.05945886m"
|
|
data-normal="-0.9392524m 0.2397608m -0.2456009m" data-orbit="-142.3926deg 86.45934deg 0.06213665m"
|
|
data-target="0.1519967m 0.01904771m -0.05945886m">
|
|
Get Away!
|
|
</button>
|
|
<button class="view-button" slot="hotspot-8" data-position="0.0094m 0.0894m -0.15103m"
|
|
data-normal="-0.3878782m 0.4957891m -0.7770094m" data-orbit="-118.6729deg 117.571deg 0.03905975m"
|
|
data-target="0.007600758m 0.06771782m -0.1386167m">
|
|
The Jump
|
|
</button>
|
|
<button class="view-button" slot="hotspot-9" data-position="-0.0658m 0.1786m -0.0183m"
|
|
data-normal="0.7857152m 0.4059967m 0.46671m" data-orbit="53.28236deg 95.91318deg 0.1102844m"
|
|
data-target="-0.07579391m 0.1393538m -0.00851791m">
|
|
The Beast
|
|
</button>
|
|
<button class="view-button" slot="hotspot-10" data-position="0.02610224m 0.01458751m -0.004978945m"
|
|
data-normal="-0.602551m 0.7856147m -0.1405055m" data-orbit="-78.89725deg 77.17752deg 0.08451112m"
|
|
data-target="0.02610223m 0.0145875m -0.004978945m">
|
|
Treasure
|
|
</button>
|
|
<button class="view-button" slot="hotspot-11" data-position="-0.1053838m 0.01610652m 0.1076345m"
|
|
data-normal="-0.624763m 0.5176854m 0.5845283m" data-orbit="10.89188deg 119.9775deg 0.03543022m"
|
|
data-target="-0.1053838m 0.01610652m 0.1076345m">
|
|
Desperation
|
|
</button>
|
|
</model-viewer>
|
|
|
|
<script type="module">
|
|
const modelViewer2 = document.querySelector("#hotspot-camera-view-demo");
|
|
const annotationClicked = (annotation) => {
|
|
let dataset = annotation.dataset;
|
|
modelViewer2.cameraTarget = dataset.target;
|
|
modelViewer2.cameraOrbit = dataset.orbit;
|
|
modelViewer2.fieldOfView = '45deg';
|
|
}
|
|
|
|
modelViewer2.querySelectorAll('button').forEach((hotspot) => {
|
|
hotspot.addEventListener('click', () => annotationClicked(hotspot));
|
|
});
|
|
</script>
|
|
|
|
<style type="text/css">
|
|
.view-button {
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
border: none;
|
|
box-sizing: border-box;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
|
|
color: rgba(0, 0, 0, 0.8);
|
|
display: block;
|
|
font-family: Futura, Helvetica Neue, sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
max-width: 128px;
|
|
overflow-wrap: break-word;
|
|
padding: 0.5em 1em;
|
|
position: absolute;
|
|
width: max-content;
|
|
height: max-content;
|
|
transform: translate3d(-50%, -50%, 0);
|
|
}
|
|
</style>
|
|
</template>
|
|
</example-snippet>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<ul>
|
|
<li class="attribution">
|
|
<a href="https://poly.google.com/view/dLHpzNdygsg">Astronaut</a> by <a
|
|
href="https://poly.google.com/user/4aEd8rQgKu2">Poly</a>,
|
|
licensed under <a href="https://creativecommons.org/licenses/by/2.0/">CC-BY</a>.
|
|
</li>
|
|
<li class="attribution">
|
|
<a href="https://github.com/dataarts/3-dreams-of-black/tree/master/deploy/files/models/soup">Horse</a> by <a
|
|
href="https://github.com/dataarts">Google Data Arts Team</a>,
|
|
licensed under <a
|
|
href="https://github.com/dataarts/3-dreams-of-black/blob/master/deploy/files/models/soup/LICENSE.txt">Creative
|
|
Commons Attribution-NonCommercial-ShareAlike</a>.
|
|
</li>
|
|
<li class="attribution">
|
|
Chair, Mixer, GeoPlanter ©Copyright 2020 <a href="https://www.shopify.com/">Shopify Inc.</a>,
|
|
licensed under <a href="https://creativecommons.org/licenses/by/4.0/">CC-BY-4.0</a>.
|
|
</li>
|
|
<li class="attribution">
|
|
<a href="https://sketchfab.com/3d-models/thor-and-the-midgard-serpent-2ef4c45caa35450db1b876a7f94ff79d">Thor
|
|
and the Midgard Serpent</a> by <a href="https://sketchfab.com/MatthijsDeRijk">Matthijs de Rijk</a>,
|
|
licensed under <a href="https://creativecommons.org/licenses/by/4.0/">CC-BY</a>.
|
|
</li>
|
|
</ul>
|
|
<div style="margin-top:24px;" class="copyright">©Copyright 2019 Google Inc. Licensed under the Apache License
|
|
2.0.</div>
|
|
<div id='footer-links'></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module" src="../../examples/built/docs-and-examples.js">
|
|
</script>
|
|
<script type="module">
|
|
(() => { init('examples-annotations'); })();
|
|
(() => { initFooterLinks(); })();
|
|
</script>
|
|
|
|
|
|
<!-- Documentation-specific dependencies: -->
|
|
<script type="module" src="../built/dependencies.js">
|
|
</script>
|
|
|
|
<!-- Loads <model-viewer> on modern browsers: -->
|
|
<script type="module" src="../../../../node_modules/@google/model-viewer/dist/model-viewer.js">
|
|
</script>
|
|
</body>
|
|
|
|
</html> |