Website: update <parallax-city> component to conditionally disable animation (#17816)

Closes: #17482

Changes:
- Updated the `<parallax-city>` component to display a static image if a
user's browser has hardware/graphics acceleration disabled.
This commit is contained in:
Eric 2024-04-05 19:09:58 -05:00 committed by GitHub
parent f0e3259765
commit 14c312361b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 112 additions and 30 deletions

View file

@ -1,7 +1,8 @@
/**
* <parallax-city>
* -----------------------------------------------------------------------------
* A button with a built-in loading spinner.
* An image of Fleet cloud city with a slight parallax scrolling effect.
* or a static image for mobile devices and browsers with hardware acceleration disabled.
*
* @type {Component}
*
@ -27,6 +28,7 @@ parasails.registerComponent('parallaxCity', {
distanceFromTopOfPage: undefined, // Used to check if the image is within the user's viewport.
distanceFromBottomOfPage: undefined, // Used to track the amount of distance between the bottom of the image, and the bottom of the page.
parallaxLayersAreCurrentlyAnimating: false,
enableAnimation: true,// Whether or not to disable the parallax scrolling animation.
};
},
@ -35,14 +37,16 @@ parasails.registerComponent('parallaxCity', {
// ╩ ╩ ╩ ╩ ╩╩═╝
template: `
<div>
<div purpose="parallax-city-container">
<div class="parallax-layer" purpose="background-cloud-2" scroll-amount=4></div>
<div class="parallax-layer" purpose="background-cloud-1" scroll-amount=6></div>
<div class="parallax-layer" purpose="small-island-2" scroll-amount=16></div>
<div class="parallax-layer" purpose="small-island-1" scroll-amount=12></div>
<div class="parallax-layer" purpose="large-island" scroll-amount=24></div>
<div class="parallax-layer" purpose="foreground-cloud-2" scroll-amount=32></div>
<div class="parallax-layer" purpose="foreground-cloud-1" scroll-amount=40></div>
<div purpose="parallax-city-container" v-if="enableAnimation">
<div class="parallax-layer" purpose="background-cloud-2" scroll-amount="4"></div>
<div class="parallax-layer" purpose="background-cloud-1" scroll-amount="6"></div>
<div class="parallax-layer" purpose="small-island-2" scroll-amount="16"></div>
<div class="parallax-layer" purpose="small-island-1" scroll-amount="12"></div>
<div class="parallax-layer" purpose="large-island" scroll-amount="24"></div>
<div class="parallax-layer" purpose="foreground-cloud-2" scroll-amount="32"></div>
<div class="parallax-layer" purpose="foreground-cloud-1" scroll-amount="40"></div>
</div>
<div purpose="static-cloud-city" v-else>
</div>
</div>
`,
@ -51,28 +55,20 @@ parasails.registerComponent('parallaxCity', {
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
// Disable animation on mobile devices.
if(bowser.isMobile) {
this.enableAnimation = false;
}
// Check for hardware/graphics acceleration.
if(bowser.chrome || bowser.opera) {
this.enableAnimation = this.isHardwareAccelerationEnabledOnChromiumBrowsers();
} else if(bowser.firefox){
this.enableAnimation = this.isHardwareAccelerationEnabledOnFirefox();
}
},
mounted: async function(){
if(!bowser.isMobile){
// Store a reference to the parent container, we'll use this to determine the elements position relative to the user's viewport.
this.parallaxCityElement = $('div[purpose="parallax-city-container"]')[0];
// Build an array of parallax layers, and set the initial bottom position of each layer to be negative the layer's scroll amount.
for(let layer of $('div.parallax-layer')) {
let scrollAmount = Number($(layer).attr('scroll-amount'));
$(layer).css('bottom', `-${scrollAmount}px`);
this.parallaxLayers.push({element: layer, scrollAmount});
}
// Determine the parallax image's position on the page/user's viewport.
this.getElementPositions();
// If the bottom of the element is within the user's viewport, update the positions of the layers.
if(this.parallaxCityElement.getBoundingClientRect().bottom > this.parallaxCityElement.offsetTop) {
this.scrollParallaxLayers();
}
// Add a scroll event listener
$(window).scroll(this.throttleParallaxScroll);
// Add a resize event listener.
$(window).resize(this.getElementPositions);
if(!this.enableAnimation) {
this.setupParallaxAnimation();
}
},
beforeDestroy: function() {
@ -89,6 +85,26 @@ parasails.registerComponent('parallaxCity', {
this.distanceFromBottomOfPage = document.body.scrollHeight - this.distanceFromTopOfPage - (this.elementHeight * .5);
this.elementBottomPosition = this.elementHeight + this.distanceFromTopOfPage;
},
setupParallaxAnimation: function() {
// Store a reference to the parent container, we'll use this to determine the elements position relative to the user's viewport.
this.parallaxCityElement = $('div[purpose="parallax-city-container"]')[0];
// Build an array of parallax layers, and set the initial bottom position of each layer to be negative the layer's scroll amount.
for(let layer of $('div.parallax-layer')) {
let scrollAmount = Number($(layer).attr('scroll-amount'));
$(layer).css('bottom', `-${scrollAmount + 1}px`);
this.parallaxLayers.push({element: layer, scrollAmount});
}
// Determine the parallax image's position on the page/user's viewport.
this.getElementPositions();
// If the bottom of the element is within the user's viewport, update the positions of the layers.
if(this.parallaxCityElement.getBoundingClientRect().bottom > this.parallaxCityElement.offsetTop) {
this.scrollParallaxLayers();
}
// Add a scroll event listener
$(window).scroll(this.throttleParallaxScroll);
// Add a resize event listener.
$(window).resize(this.getElementPositions);
},
scrollParallaxLayers: function() {
if(!this.parallaxLayersAreCurrentlyAnimating) {
this.parallaxLayersAreCurrentlyAnimating = true;
@ -111,6 +127,45 @@ parasails.registerComponent('parallaxCity', {
setTimeout(()=>{
this.parallaxLayersAreCurrentlyAnimating = false;
}, 100);
}
},
isHardwareAccelerationEnabledOnChromiumBrowsers: function() {
let isHardwareAccelerationEnabled = true;
// For Chromium based browsers, we'll check the vendor of the user's graphics card.
// See https://gist.github.com/cvan/042b2448fcecefafbb6a91469484cdf8 for more info about this method.
let canvas = document.createElement('canvas');
let webGLContext = canvas.getContext('webgl');
if (!webGLContext) {
// If webGLContext is undefined, we'll assume the user has hardware acceleration disabled, and we won't animate the parallax layers.
isHardwareAccelerationEnabled = false;
} else {
// Otherwise, we'll check to see if the 'Vendor' of this users GPU.
let debugInfo = webGLContext.getExtension('WEBGL_debug_renderer_info');
let vendor = webGLContext.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
// If vendor is "Google Inc. (Google)" or "Google Inc.", we can safely assume this user doesn't have hardware acceleration enabled and we'll disable the parallax animation.
if(vendor === 'Google Inc. (Google)' || vendor === 'Google Inc.') {
isHardwareAccelerationEnabled = false;
}
}
return isHardwareAccelerationEnabled;
},
isHardwareAccelerationEnabledOnFirefox: function() {
// For Firefox, the method we use for chrome does not always work.
// Instead, we'll run two tests, one with forced software rendering, and one without to see if the results are the same.
// See https://stackoverflow.com/a/77170999 for more info about this method.
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d', { willReadFrequently: false });
ctx.moveTo(0, 0);
ctx.lineTo(120, 121);
ctx.stroke();
let firstTestResults = ctx.getImageData(0, 0, 200, 200).data.join();
let canvasForSoftwareRenderingTest = document.createElement('canvas');
let ctxWithSoftwareRendering = canvasForSoftwareRenderingTest.getContext('2d', { willReadFrequently: true });// willReadFrequently will force software rendering
ctxWithSoftwareRendering.moveTo(0, 0);
ctxWithSoftwareRendering.lineTo(120, 121); // HWA is bad at obliques
ctxWithSoftwareRendering.stroke();
let softwareRenderingTestResults = ctxWithSoftwareRendering.getImageData(0, 0, 200, 200).data.join();
// If the results from the software rendering test are identical to the first test, we can assume the user has hardware acceleration disabled.
return firstTestResults !== softwareRenderingTestResults;
},
}
});

View file

@ -14,6 +14,14 @@
display: flex;
flex-direction: column;
justify-content: flex-end;
[purpose='static-cloud-city'] {
background-image: url('/images/parallax-cloud-city/cloud-city-static-7050x600@2x.png');
background-size: cover;
background-position: center bottom;
background-repeat: no-repeat;
height: 456px;
position: relative;
}
[purpose='parallax-city-container'] {
height: 456px;
position: relative;
@ -75,11 +83,17 @@
[purpose='parallax-city-container'] {
height: 400px;
}
[purpose='static-cloud-city'] {
height: 400px;
}
}
@media (max-width: 768px) {
[purpose='parallax-city-container'] {
height: 300px;
}
[purpose='static-cloud-city'] {
height: 300px;
}
}
@media (max-width: 575px) {
[purpose='parallax-city-container'] {
@ -92,10 +106,23 @@
display: none;
}
}
[purpose='static-cloud-city'] {
height: 300px;
background-image: url('/images/parallax-cloud-city/cloud-city-static-576x300@2x.png');
background-size: cover;
background-position: center bottom;
background-repeat: no-repeat;
.parallax-layer {
display: none;
}
}
}
@media (max-width: 375px) {
[purpose='parallax-city-container'] {
height: 200px;
}
[purpose='static-cloud-city'] {
height: 200px;
}
}
}