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

Announce camera orientation #325

Merged
merged 1 commit into from
Feb 1, 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
1 change: 0 additions & 1 deletion examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ <h2>Demos &amp; Scenarios</h2>
src="assets/Astronaut.glb"
alt="A 3D model of an astronaut"
controls
auto-rotate
background-color="#455A64">
</model-viewer>
</template>
Expand Down
45 changes: 42 additions & 3 deletions src/features/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@
* limitations under the License.
*/

import {PerspectiveCamera, Vector3} from 'three';
import {PerspectiveCamera, Spherical, Vector3} from 'three';

import {$ariaLabel, $needsRender, $onModelLoad, $onResize, $scene, $tick} from '../model-viewer-base.js';
import {FRAMED_HEIGHT} from '../three-components/ModelScene.js';
import {SmoothControls} from '../three-components/SmoothControls.js';

const HALF_PI = Math.PI / 2.0;
const THIRD_PI = Math.PI / 3.0;
const QUARTER_PI = HALF_PI / 2.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I'm hungry 🍰

const PHI = 2.0 * Math.PI;

const AZIMUTHAL_QUADRANT_LABELS = ['front', 'right', 'back', 'left'];
const POLAR_TRIENT_LABELS = ['upper-', '', 'lower-'];

const ORBIT_NEAR_PLANE = 0.01;
const ORBIT_FAR_PLANE = 1000;

Expand All @@ -43,6 +51,8 @@ const $waitingToPromptUser = Symbol('waitingToPromptUser');
const $userPromptedOnce = Symbol('userPromptedOnce');
const $idleTime = Symbol('idleTime');

const $lastSpherical = Symbol('lastSpherical');

export const $promptElement = Symbol('promptElement');

export const ControlsMixin = (ModelViewerElement) => {
Expand All @@ -69,6 +79,8 @@ export const ControlsMixin = (ModelViewerElement) => {
this[$orbitCamera].updateProjectionMatrix();
this[$controls] = null;

this[$lastSpherical] = new Spherical();

this[$changeHandler] = () => this[$onChange]();
this[$focusHandler] = () => this[$onFocus]();
this[$blurHandler] = () => this[$onBlur]();
Expand Down Expand Up @@ -170,8 +182,10 @@ export const ControlsMixin = (ModelViewerElement) => {
// original, non-prompt label if appropriate. If the user has already
// interacted, they no longer need to hear the prompt. Otherwise, they
// will hear it again after the idle prompt threshold has been crossed.
if (canvas.getAttribute('aria-label') === IDLE_PROMPT) {
canvas.setAttribute('aria-label', this[$ariaLabel]);
const ariaLabel = this[$ariaLabel];

if (canvas.getAttribute('aria-label') !== ariaLabel) {
canvas.setAttribute('aria-label', ariaLabel);
}

// NOTE(cdata): When focused, if the user has yet to interact with the
Expand Down Expand Up @@ -202,6 +216,31 @@ export const ControlsMixin = (ModelViewerElement) => {
if (this[$userPromptedOnce]) {
this[$shouldPromptUserToInteract] = false;
}

const {theta: lastTheta, phi: lastPhi} = this[$lastSpherical];
const {theta, phi} =
this[$controls].getCameraSpherical(this[$lastSpherical]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


const lastAzimuthalQuadrant =
(4 + Math.floor(((lastTheta % PHI) + QUARTER_PI) / HALF_PI)) % 4;
const azimuthalQuadrant =
(4 + Math.floor(((theta % PHI) + QUARTER_PI) / HALF_PI)) % 4;

const lastPolarTrient = Math.floor(lastPhi / THIRD_PI);
const polarTrient = Math.floor(phi / THIRD_PI);

if (azimuthalQuadrant !== lastAzimuthalQuadrant ||
polarTrient !== lastPolarTrient) {
const {canvas} = this[$scene];
const azimuthalQuadrantLabel =
AZIMUTHAL_QUADRANT_LABELS[azimuthalQuadrant];
const polarTrientLabel = POLAR_TRIENT_LABELS[polarTrient];

const ariaLabel =
`View from stage ${polarTrientLabel}${azimuthalQuadrantLabel}`;

canvas.setAttribute('aria-label', ariaLabel);
}
}
};
};
64 changes: 62 additions & 2 deletions src/test/features/controls-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {$controls, $promptElement, ControlsMixin, IDLE_PROMPT, IDLE_PROMPT_THRES
import ModelViewerElementBase, {$scene} from '../../model-viewer-base.js';
import {assetPath, dispatchSyntheticEvent, rafPasses, timePasses, until, waitForEvent} from '../helpers.js';

import {settleControls} from '../three-components/SmoothControls-spec.js';

const expect = chai.expect;

const interactWith = (element) => {
Expand Down Expand Up @@ -85,6 +87,10 @@ suite('ModelViewerElementBase with ControlsMixin', () => {
});

suite('a11y', () => {
setup(async () => {
await rafPasses();
});

test('prompts user to interact when focused', async () => {
const {canvas} = element[$scene];
const promptElement = element[$promptElement];
Expand All @@ -109,6 +115,8 @@ suite('ModelViewerElementBase with ControlsMixin', () => {
const promptElement = element[$promptElement];
const originalLabel = canvas.getAttribute('aria-label');

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

canvas.focus();

await timePasses();
Expand All @@ -117,11 +125,63 @@ suite('ModelViewerElementBase with ControlsMixin', () => {

await timePasses(IDLE_PROMPT_THRESHOLD_MS + 100);

expect(canvas.getAttribute('aria-label')).to.be.equal(originalLabel);

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

test('announces camera orientation when orbiting horizontally', () => {
const {canvas} = element[$scene];
const controls = element[$controls];

controls.setOrbit(-Math.PI / 2.0);
settleControls(controls);

expect(canvas.getAttribute('aria-label'))
.to.be.equal('View from stage left');

controls.setOrbit(Math.PI / 2.0);
settleControls(controls);

expect(canvas.getAttribute('aria-label'))
.to.be.equal('View from stage right');

controls.adjustOrbit(-Math.PI / 2.0, 0, 0);
settleControls(controls);

expect(canvas.getAttribute('aria-label'))
.to.be.equal('View from stage back');

controls.adjustOrbit(Math.PI, 0, 0);
settleControls(controls);

expect(canvas.getAttribute('aria-label'))
.to.be.equal('View from stage front');
});

test('announces camera orientation when orbiting vertically', () => {
const {canvas} = element[$scene];
const controls = element[$controls];

controls.setOrbit(0, 0);
settleControls(controls);

expect(canvas.getAttribute('aria-label'))
.to.be.equal('View from stage upper-front');

controls.adjustOrbit(0, -Math.PI / 2.0, 0);
settleControls(controls);

expect(canvas.getAttribute('aria-label'))
.to.be.equal('View from stage front');

controls.adjustOrbit(0, -Math.PI / 2.0, 0);
settleControls(controls);

expect(canvas.getAttribute('aria-label'))
.to.be.equal('View from stage lower-front');
});
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/test/three-components/SmoothControls-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const cameraIsLookingAt = (camera, position) => {
/**
* Settle controls by performing 50 frames worth of updates
*/
const settleControls = controls =>
export const settleControls = controls =>
controls.update(performance.now, FIFTY_FRAME_DELTA);

suite('SmoothControls', () => {
Expand Down