Skip to content

Commit

Permalink
Fix various bugs related to interaction prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Joel authored and cdata committed Apr 3, 2019
1 parent ed45827 commit 161a8fd
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 18 deletions.
9 changes: 9 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,15 @@ <h4>experimental-pmrem</h4>
the built-in scene lights. Refer to <a href="https://wiki.jmonkeyengine.org/jme3/advanced/pbr_part3.html" target="_blank">this article</a>
for in-depth details related to this lighting mode.</p>
</li>
<li>
<h4>interaction-prompt-threshold</h4>
<p>When camera-controls are enabled, &lt;model-viewer&gt; will
prompt the user visually (and audibly, for screen readers) to
interact if they focus it but don't interact with it for some time.
This attribute allows you to set how long &lt;model-viewer&gt;
should wait (in milliseconds) before prompting to interact. Defaults
to 3000.</p>
</li>
<li>
<h4>ios-src</h4>
<p>The url to a <a href="https://graphics.pixar.com/usd/docs/Usdz-File-Format-Specification.html">USDZ</a> model which will be used on <a href="https://www.apple.com/ios/augmented-reality/">supported iOS 12+ devices</a> via <a href="https://developer.apple.com/videos/play/wwdc2018/603/">AR Quick Look</a> on Safari. See <a href="https://github.com/GoogleWebComponents/model-viewer#augmented-reality">Augmented Reality</a>.</p>
Expand Down
3 changes: 1 addition & 2 deletions src/assets/controls-svg.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="250px" height="200px" viewBox="0 0 250 200" fill="transparent">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="250px" height="200px" viewBox="0 0 250 200" fill="transparent" focusable="false" aria-hidden="true">
<!-- Rotation arc -->
<path id="orbit"
d="M50,75 C55,50 195,50 200,75 C195,100 55,100 50,75"
Expand All @@ -23,7 +23,6 @@ export default `
stroke-dasharray="140 220"
stroke="white"
stroke-width="7"
fill="transparent">
<animate attributeName="stroke-dashoffset"
dur="1.75s"
Expand Down
2 changes: 1 addition & 1 deletion src/assets/view-in-ar-material-svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export default `
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve" focusable="false" aria-hidden="true">
<rect fill="none" width="24" height="24"/>
<g>
<path d="M18.25,7.6l-5.5-3.18c-0.46-0.27-1.04-0.27-1.5,0L5.75,7.6C5.29,7.87,5,8.36,5,8.9v6.35c0,0.54,0.29,1.03,0.75,1.3
Expand Down
52 changes: 43 additions & 9 deletions src/features/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ const POLAR_TRIENT_LABELS = ['upper-', '', 'lower-'];
const ORBIT_CAMERA_NEAR_PLANE = FRAMED_HEIGHT / 10.0;
const ORBIT_CAMERA_FAR_PLANE = FRAMED_HEIGHT * 10.0;

export const IDLE_PROMPT_THRESHOLD_MS = 3000;
export const IDLE_PROMPT =
export const DEFAULT_INTERACTION_PROMPT_THRESHOLD = 3000;
export const INTERACTION_PROMPT =
'Use mouse, touch or arrow keys to control the camera!';

export const $controls = Symbol('controls');
Expand All @@ -58,9 +58,12 @@ const $defaultCamera = Symbol('defaultCamera');
const $blurHandler = Symbol('blurHandler');
const $focusHandler = Symbol('focusHandler');
const $changeHandler = Symbol('changeHandler');
const $promptTransitionendHandler = Symbol('promptTransitionendHandler');

const $onBlur = Symbol('onBlur');
const $onFocus = Symbol('onFocus');
const $onChange = Symbol('onChange');
const $onPromptTransitionend = Symbol('onPromptTransitionend');

const $shouldPromptUserToInteract = Symbol('shouldPromptUserToInteract');
const $waitingToPromptUser = Symbol('waitingToPromptUser');
Expand All @@ -80,7 +83,11 @@ export const ControlsMixin = (ModelViewerElement:
{type: String, attribute: 'camera-orbit', hasChanged: () => true})
cameraOrbit: string = DEFAULT_CAMERA_ORBIT;

protected[$promptElement]: HTMLElement|null;
@property({type: Number, attribute: 'interaction-prompt-threshold'})
interactionPromptThreshold: number =
DEFAULT_INTERACTION_PROMPT_THRESHOLD;

protected[$promptElement]: Element;

protected[$defaultCamera]: PerspectiveCamera;
protected[$orbitCamera]: PerspectiveCamera;
Expand All @@ -98,12 +105,15 @@ export const ControlsMixin = (ModelViewerElement:
protected[$focusHandler]: () => void = () => this[$onFocus]();
protected[$blurHandler]: () => void = () => this[$onBlur]();

protected[$promptTransitionendHandler]:
() => void = () => this[$onPromptTransitionend]();

constructor() {
super();
const scene = (this as any)[$scene];

this[$promptElement] =
this.shadowRoot!.querySelector('.controls-prompt');
this.shadowRoot!.querySelector('.controls-prompt')!;

this[$defaultCamera] = scene.getCamera();
this[$orbitCamera] = this[$defaultCamera].clone();
Expand All @@ -127,12 +137,17 @@ export const ControlsMixin = (ModelViewerElement:
connectedCallback() {
super.connectedCallback();

this[$promptTransitionendHandler]();
this[$promptElement].addEventListener(
'transitionend', this[$promptTransitionendHandler]);
this[$controls].addEventListener('change', this[$changeHandler]);
}

disconnectedCallback() {
super.disconnectedCallback();

this[$promptElement].removeEventListener(
'transitionend', this[$promptTransitionendHandler]);
this[$controls].removeEventListener('change', this[$changeHandler]);
}

Expand Down Expand Up @@ -189,11 +204,13 @@ export const ControlsMixin = (ModelViewerElement:
super[$tick](time, delta);

if (this[$waitingToPromptUser]) {
this[$idleTime] += delta;
if (this.loaded) {
this[$idleTime] += delta;
}

if (this[$idleTime] > IDLE_PROMPT_THRESHOLD_MS) {
if (this[$idleTime] > this.interactionPromptThreshold) {
(this as any)[$scene].canvas.setAttribute(
'aria-label', IDLE_PROMPT);
'aria-label', INTERACTION_PROMPT);

// NOTE(cdata): After notifying users that the controls are
// available, we flag that the user has been prompted at least
Expand All @@ -203,7 +220,7 @@ export const ControlsMixin = (ModelViewerElement:
this[$userPromptedOnce] = true;
this[$waitingToPromptUser] = false;

this[$promptElement]!.classList.add('visible');
this[$promptElement].classList.add('visible');
}
}

Expand Down Expand Up @@ -288,6 +305,23 @@ export const ControlsMixin = (ModelViewerElement:
}
}

[$onPromptTransitionend]() {
const svg = this[$promptElement].querySelector('svg');

if (svg == null) {
return;
}

// NOTE(cdata): We need to make sure that SVG animations are paused
// when the prompt is not visible, otherwise we may a significant
// compositing cost even while the prompt is at opacity 0.
if (this[$promptElement].classList.contains('visible')) {
svg.unpauseAnimations();
} else {
svg.pauseAnimations();
}
}

[$onResize](e: Event) {
super[$onResize](e);
this[$updateOrbitCamera]();
Expand Down Expand Up @@ -324,7 +358,7 @@ export const ControlsMixin = (ModelViewerElement:

[$onBlur]() {
this[$waitingToPromptUser] = false;
this[$promptElement]!.classList.remove('visible');
this[$promptElement].classList.remove('visible');
}

[$onChange]() {
Expand Down
45 changes: 39 additions & 6 deletions src/test/features/controls-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* limitations under the License.
*/

import {$controls, $promptElement, ControlsMixin, IDLE_PROMPT, IDLE_PROMPT_THRESHOLD_MS} from '../../features/controls.js';
import {$controls, $promptElement, ControlsMixin, DEFAULT_INTERACTION_PROMPT_THRESHOLD, INTERACTION_PROMPT} from '../../features/controls.js';
import ModelViewerElementBase, {$scene} from '../../model-viewer-base.js';
import {FRAMED_HEIGHT} from '../../three-components/ModelScene.js';
import {assetPath, dispatchSyntheticEvent, rafPasses, timePasses, until, waitForEvent} from '../helpers.js';
Expand Down Expand Up @@ -229,28 +229,61 @@ suite('ModelViewerElementBase with ControlsMixin', () => {

canvas.focus();

await until(() => canvas.getAttribute('aria-label') === IDLE_PROMPT);
await until(
() => canvas.getAttribute('aria-label') === INTERACTION_PROMPT);

expect(promptElement.classList.contains('visible')).to.be.equal(true);
});

test(
'does not prompt users to interact before a model is loaded',
async () => {
Object.defineProperty(
element, 'loaded', {value: false, configurable: true});

element.interactionPromptThreshold = 500;

const {canvas} = element[$scene];
const promptElement = element[$promptElement];
const controls = element[$controls];

settleControls(controls);

await rafPasses();

canvas.focus();

await timePasses(element.interactionPromptThreshold + 100);

expect(promptElement.classList.contains('visible'))
.to.be.equal(false);

Object.defineProperty(
element, 'loaded', {value: true, configurable: true});

await timePasses(element.interactionPromptThreshold + 100);

expect(promptElement.classList.contains('visible'))
.to.be.equal(true);
});

test('does not prompt if user already interacted', async () => {
const {canvas} = element[$scene];
const promptElement = element[$promptElement];
const originalLabel = canvas.getAttribute('aria-label');

expect(originalLabel).to.not.be.equal(IDLE_PROMPT);
expect(originalLabel).to.not.be.equal(INTERACTION_PROMPT);

canvas.focus();

await timePasses(IDLE_PROMPT_THRESHOLD_MS / 2.0);
await timePasses(DEFAULT_INTERACTION_PROMPT_THRESHOLD / 2.0);

interactWith(canvas);

await timePasses(IDLE_PROMPT_THRESHOLD_MS + 100);
await timePasses(DEFAULT_INTERACTION_PROMPT_THRESHOLD + 100);

expect(canvas.getAttribute('aria-label'))
.to.not.be.equal(IDLE_PROMPT);
.to.not.be.equal(INTERACTION_PROMPT);
expect(promptElement.classList.contains('visible'))
.to.be.equal(false);
});
Expand Down

0 comments on commit 161a8fd

Please sign in to comment.