Website: Add parallax image to homepage and landing pages. (#16035)

Closes: #14987


Changes:
- Added a new component that displays a Fleet cloud city image with a
parallax effect when scrolled.

---------

Co-authored-by: Mike Thomas <78363703+mike-j-thomas@users.noreply.github.com>
This commit is contained in:
Eric 2024-01-23 23:18:07 -06:00 committed by GitHub
parent bcc1799e07
commit 11fda29c87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 288 additions and 21 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -0,0 +1,126 @@
/**
* <parallax-city>
* -----------------------------------------------------------------------------
* A button with a built-in loading spinner.
*
* @type {Component}
*
* @event click [emitted when clicked]
* -----------------------------------------------------------------------------
*/
parasails.registerComponent('parallaxCity', {
// ╔═╗╦═╗╔═╗╔═╗╔═╗
// ╠═╝╠╦╝║ ║╠═╝╚═╗
// ╩ ╩╚═╚═╝╩ ╚═╝
props: ['isMobile'],
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
data: function (){
return {
parallaxCityElement: undefined,
elementBottomPosition: undefined,
elementHeight: undefined,
distanceFromTopOfPage: undefined,
distanceFromBottomOfPage: undefined,
isAnimating: false,
};
},
// ╦ ╦╔╦╗╔╦╗╦
// ╠═╣ ║ ║║║║
// ╩ ╩ ╩ ╩ ╩╩═╝
template: `
<div>
<div purpose="parallax-city-container">
<div class="parallax-layer" purpose="background-cloud-3" scroll-amount=12></div>
<div class="parallax-layer" purpose="background-cloud-2" scroll-amount=28></div>
<div class="parallax-layer" purpose="small-island-2" scroll-amount=20></div>
<div class="parallax-layer" purpose="small-island-1" scroll-amount=40></div>
<div class="parallax-layer" purpose="background-cloud-1" scroll-amount=40></div>
<div class="parallax-layer" purpose="large-island" scroll-amount=60></div>
<div class="parallax-layer" purpose="foreground-cloud-2" scroll-amount=100></div>
<div class="parallax-layer" purpose="foreground-cloud-1" scroll-amount=120></div>
</div>
</div>
`,
// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
},
mounted: async function(){
if(!this.isMobile){
this.parallaxCityElement = document.querySelector('[purpose="parallax-city-container"]');
this.elementHeight = this.parallaxCityElement.clientHeight;
this.distanceFromTopOfPage = this.parallaxCityElement.offsetTop;
this.distanceFromBottomOfPage = document.body.scrollHeight - this.distanceFromTopOfPage - (this.elementHeight * .5);
this.elementBottomPosition = this.elementHeight + this.distanceFromTopOfPage;
let parallaxCityElementPosition = this.parallaxCityElement.getBoundingClientRect();
if(parallaxCityElementPosition.bottom > this.distanceFromTopOfPage) {
this.handleParallaxScroll();
}
this.parallaxCityElement.querySelectorAll('div.parallax-layer').forEach((layer)=>{
let initialPosition = layer.getAttribute('scroll-amount');
layer.style.bottom = `-${Number(initialPosition) + 4}px`;
});
window.addEventListener('scroll', this.onScroll);
window.addEventListener('resize', this.updateElementPositions);
window.addEventListener('orientationchange', this.updateElementPositions);
}
},
beforeDestroy: function() {
if(!this.isMobile){
window.removeEventListener('scroll', this.onScroll);
window.removeEventListener('resize', this.updateElementPositions);
window.removeEventListener('orientationchange', this.updateElementPositions);
}
},
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
updateElementPositions: function() {
this.elementHeight = this.parallaxCityElement.clientHeight;
this.distanceFromTopOfPage = this.parallaxCityElement.offsetTop;
this.elementBottomPosition = this.elementHeight + this.distanceFromTopOfPage;
},
onScroll: function() {
if(!this.isAnimating){
this.isAnimating = true;
window.requestAnimationFrame(this.handleParallaxScroll);
}
return;
},
handleParallaxScroll: function() {
let viewportBottom = window.scrollY + window.innerHeight;
let percentageScrolled;
if (this.parallaxCityElement.offsetTop < viewportBottom) {
let visibleHeight = viewportBottom - Math.max(this.distanceFromTopOfPage, window.scrollY);
percentageScrolled = visibleHeight / (this.distanceFromBottomOfPage + (this.elementHeight / 2 ));
} else {
percentageScrolled = 0;
}
if(percentageScrolled > 1){
percentageScrolled = 1;
}
percentageScrolled = percentageScrolled.toFixed(4);
if(percentageScrolled > .25){// When the element has been scrolled down 25%, start adjusting the position of layers.
let adjustedPercentage = (percentageScrolled - .25) * 4/3;
this.parallaxCityElement.querySelectorAll('div.parallax-layer').forEach((layer) => {
let scrollAmount = layer.getAttribute('scroll-amount');
let movement = adjustedPercentage * scrollAmount;
layer.style.transform = 'translateY(-' + movement + 'px)';
});
}
this.isAnimating = false;
},
}
});

View file

@ -0,0 +1,111 @@
/**
* <parallax-city>
*
* App-wide styles for our ajax buttons.
*/
[parasails-component='parallax-city'] {
background: linear-gradient(180deg, #E9F4F4 0%, #FFFFFF 650px);
background-size: auto 100%;
background-position: center bottom;
background-repeat: no-repeat;
max-height: 600px;
display: flex;
flex-direction: column;
justify-content: flex-end;
[purpose='parallax-city-container'] {
height: 600px;
position: relative;
.parallax-layer {
position: absolute;
right: 0;
bottom: -1px;
width: 100%;
height: 100%;
will-change: transform;
background-size: auto 100%;
}
[purpose='foreground-cloud-1'] {
background-image: url('/images/parallax-cloud-city/1-cloud-7050x600@2x.png');
background-position: center bottom;
background-repeat: no-repeat;
z-index: 100;
}
[purpose='foreground-cloud-2'] {
background-image: url('/images/parallax-cloud-city/2-cloud-7050x600@2x.png');
background-position: center bottom;
background-repeat: no-repeat;
z-index: 99;
}
[purpose='large-island'] {
background-image: url('/images/parallax-cloud-city/4-island-7050x600@2x.png');
background-position: center bottom;
background-repeat: no-repeat;
z-index: 97;
}
[purpose='background-cloud-1'] {
background-image: url('/images/parallax-cloud-city/5-cloud-7050x600@2x.png');
background-position: center bottom;
background-repeat: no-repeat;
z-index: 96;
}
[purpose='small-island-1'] {
background-image: url('/images/parallax-cloud-city/6-island-7050x600@2x.png');
background-position: center bottom;
background-repeat: no-repeat;
z-index: 95;
}
[purpose='small-island-2'] {
background-image: url('/images/parallax-cloud-city/7-island-7050x600@2x.png');
background-position: center bottom;
background-repeat: no-repeat;
z-index: 95;
}
[purpose='background-cloud-2'] {
background-image: url('/images/parallax-cloud-city/8-cloud-7050x600@2x.png');
background-position: center bottom;
background-repeat: no-repeat;
z-index: 1;
}
[purpose='background-cloud-3'] {
background-image: url('/images/parallax-cloud-city/9-cloud-7050x600@2x.png');
background-position: center bottom;
background-repeat: no-repeat;
z-index: 1;
}
}
@media (max-width: 1399px) {
[purpose='parallax-city-container'] {
height: 500px;
}
}
@media (max-width: 991px) {
[purpose='parallax-city-container'] {
height: 400px;
}
}
@media (max-width: 768px) {
[purpose='parallax-city-container'] {
height: 300px;
}
}
@media (max-width: 575px) {
[purpose='parallax-city-container'] {
height: 300px;
background-image: url('/images/parallax-cloud-city/cloud-city-static-576x300@2x.png');
background-size: contain;
background-position: center bottom;
background-repeat: no-repeat;
.parallax-layer {
display: none;
}
}
}
@media (max-width: 375px) {
[purpose='parallax-city-container'] {
height: 200px;
}
}
}

View file

@ -25,6 +25,7 @@
@import 'components/bar-chart.component.less';
@import 'components/call-to-action.component.less';
@import 'components/scrollable-tweets.component.less';
@import 'components/parallax-city.component.less';
// Per-page styles
@import 'pages/homepage.less';

View file

@ -218,6 +218,7 @@
<script src="/js/components/js-timestamp.component.js"></script>
<script src="/js/components/modal.component.js"></script>
<script src="/js/components/open-positions.component.js"></script>
<script src="/js/components/parallax-city.component.js"></script>
<script src="/js/components/rituals.component.js"></script>
<script src="/js/components/scrollable-tweets.component.js"></script>
<script src="/js/components/stripe-card-element.component.js"></script>

View file

@ -323,6 +323,7 @@
<script src="/js/components/js-timestamp.component.js"></script>
<script src="/js/components/modal.component.js"></script>
<script src="/js/components/open-positions.component.js"></script>
<script src="/js/components/parallax-city.component.js"></script>
<script src="/js/components/rituals.component.js"></script>
<script src="/js/components/scrollable-tweets.component.js"></script>
<script src="/js/components/stripe-card-element.component.js"></script>

View file

@ -410,6 +410,7 @@
<script src="/js/components/js-timestamp.component.js"></script>
<script src="/js/components/modal.component.js"></script>
<script src="/js/components/open-positions.component.js"></script>
<script src="/js/components/parallax-city.component.js"></script>
<script src="/js/components/rituals.component.js"></script>
<script src="/js/components/scrollable-tweets.component.js"></script>
<script src="/js/components/stripe-card-element.component.js"></script>

View file

@ -192,11 +192,7 @@
</div>
</div>
<%/* Cloud city banner */%>
<div class="d-flex flex-column" purpose="bottom-cloud-city-banner">
<img alt="A glass city floating on top of fluffy white clouds" class="d-none d-lg-flex" src="/images/homepage-cloud-city-banner-lg-1600x375@2x.png">
<img alt="A glass city floating on top of fluffy white clouds" class="d-none d-md-flex d-lg-none" src="/images/homepage-cloud-city-banner-md-990x375@2x.png">
<img alt="A glass city floating on top of fluffy white clouds" class="d-flex d-md-none" src="/images/homepage-cloud-city-banner-sm-375x168@2x.png">
</div>
<parallax-city :is-mobile="bowser.mobile"></parallax-city>
<modal purpose="video-modal" v-if="modal === 'austin-anderson'" @close="closeModal()">
<iframe width="560" height="315" src="https://www.youtube.com/embed/G5Ry_vQPaYc?si=vv0AfRe30yssWWRM&amp;rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</modal>

View file

@ -120,10 +120,49 @@
</div>
</div>
<%/* Cloud city banner */%>
<div class="d-flex flex-column" purpose="bottom-cloud-city-banner">
<img alt="A glass city floating on top of fluffy white clouds" class="d-none d-lg-flex" src="/images/homepage-cloud-city-banner-lg-1600x375@2x.png">
<img alt="A glass city floating on top of fluffy white clouds" class="d-none d-md-flex d-lg-none" src="/images/homepage-cloud-city-banner-md-990x375@2x.png">
<img alt="A glass city floating on top of fluffy white clouds" class="d-flex d-md-none" src="/images/homepage-cloud-city-banner-sm-375x168@2x.png">
</div>
<parallax-city :is-mobile="bowser.mobile"></parallax-city>
<modal v-if="modal === 'mdm'" @close="closeModal()" data-backdrop="false" v-cloak purpose="modal">
<div class="container p-0 d-flex flex-column justify-content-center align-items-center" purpose="mdm-modal">
<div purpose="modal-form" v-if="!showSignupFormSuccess">
<div class="modal-header pb-2">
<h2 class="text-center mb-2">Is it any good?</h2>
<p>Fill out the form below to see Dave bootstrapping a macOS device with MDM.</p>
</div>
<ajax-form action="deliverMdmDemoEmail" class="mdm-demo-video w-100" :syncing.sync="syncing" :cloud-error.sync="cloudError" :form-data="formData" :form-rules="formRules" :form-errors.sync="formErrors" @submitted="submittedForm()">
<div class="form-group">
<label for="emailAddress">Work email</label>
<input class="form-control" id="emailAddress" :class="[formErrors.emailAddress ? 'is-invalid' : '']" v-model.trim="formData.emailAddress" @input="typeClearOneFormError('emailAddress')">
<div class="invalid-feedback" v-if="formErrors.emailAddress" focus-first>Please enter a valid email address</div>
</div>
<div class="form-group">
<p style="padding-bottom: 12px;" class="mb-0 font-weight-bold">How many hosts do you have?</p>
<div class="form-check">
<input class="form-check-input" type="radio" id="more-than-1000" value="true" v-model="formData.hasOverOneThousandHosts" :class="[formErrors.hasOverOneThousandHosts ? 'is-invalid' : '']">
<label class="form-check-label font-weight-normal" for="more-than-1000">
More than 1,000
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="less-than-1000" value="false" v-model="formData.hasOverOneThousandHosts" :class="[formErrors.hasOverOneThousandHosts ? 'is-invalid' : '']">
<label class="form-check-label font-weight-normal" for="less-than-1000">
Less than 1,000
</label>
<div class="invalid-feedback" style="margin-left: -20px;" v-if="formErrors.hasOverOneThousandHosts" focus-first>Please choose one option</div>
</div>
</div>
<cloud-error v-if="cloudError"></cloud-error>
<div class="border-0 justify-content-center">
<ajax-button purpose="submit-button" spinner="true" type="submit" :syncing="syncing" class="btn btn-sm btn-block btn-primary">Submit</ajax-button>
</div>
</ajax-form>
<p class="mb-0 mt-4"><a href="/handbook/company/why-this-way#why-dont-we-sell-like-everyone-else" target="_blank">We will never spam you</a>.</p>
</div>
<div purpose="modal-form" class="text-center" v-else>
<h2 class="mb-2">Thank you!</h2>
<p class="mb-4">We just sent you an email in case you need to watch it again.</p>
<a purpose="cta-button" class="btn btn-primary btn-block" href="https://play.goconsensus.com/ue326b4dd" target="_blank"><p class="mb-0">Watch Dave's MDM demo</p></a>
</div>
</div>
</modal>
</div>
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>

View file

@ -229,12 +229,7 @@
</div>
<%/* Cloud city banner */%>
<div class="d-flex flex-column" purpose="bottom-cloud-city-banner">
<img alt="A glass city floating on top of fluffy white clouds" class="d-none d-lg-flex" src="/images/homepage-cloud-city-banner-lg-1600x375@2x.png">
<img alt="A glass city floating on top of fluffy white clouds" class="d-none d-md-flex d-lg-none" src="/images/homepage-cloud-city-banner-md-990x375@2x.png">
<img alt="A glass city floating on top of fluffy white clouds" class="d-flex d-md-none" src="/images/homepage-cloud-city-banner-sm-375x168@2x.png">
</div>
<parallax-city :is-mobile="bowser.mobile"></parallax-city>
<modal purpose="video-modal" v-if="modal === 'austin-anderson'" @close="closeModal()">
<iframe width="560" height="315" src="https://www.youtube.com/embed/G5Ry_vQPaYc?si=vv0AfRe30yssWWRM&amp;rel=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</modal>

View file

@ -151,11 +151,7 @@
</div>
</div>
<%/* Cloud city banner */%>
<div class="d-flex flex-column" purpose="bottom-cloud-city-banner">
<img alt="A glass city floating on top of fluffy white clouds" class="d-none d-lg-flex" src="/images/homepage-cloud-city-banner-lg-1600x375@2x.png">
<img alt="A glass city floating on top of fluffy white clouds" class="d-none d-md-flex d-lg-none" src="/images/homepage-cloud-city-banner-md-990x375@2x.png">
<img alt="A glass city floating on top of fluffy white clouds" class="d-flex d-md-none" src="/images/homepage-cloud-city-banner-sm-375x168@2x.png">
</div>
<parallax-city :is-mobile="bowser.mobile"></parallax-city>
<modal purpose="video-modal" v-if="modal === 'austin-anderson'" @close="closeModal()">
<iframe width="560" height="315" src="https://www.youtube.com/embed/G5Ry_vQPaYc?si=vv0AfRe30yssWWRM&amp;rel=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</modal>