Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix various bugs related to interaction prompt #457

Merged
merged 1 commit into from
Apr 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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