Skip to content
This repository has been archived by the owner on Mar 14, 2020. It is now read-only.

Commit

Permalink
Fix background-size + factor in the offset #1
Browse files Browse the repository at this point in the history
  • Loading branch information
danistefanovic committed Sep 22, 2017
1 parent 6b3baa6 commit bae62d6
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 51 deletions.
117 changes: 77 additions & 40 deletions src/LazyHero.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import styled from 'styled-components';

import { resizeToCover, scrolledOverPercent } from './utils';

const Cover = styled.div`
position: absolute;
top: 0;
Expand All @@ -19,10 +21,10 @@ const Root = styled.div`

const Img = styled(Cover)`
background-attachment: ${props => (props.isFixed ? 'fixed' : 'scroll')};
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
background-image: url(${(props => props.src)});
background-position: center;
background-repeat: no-repeat;
background-size: ${props => (props.width ? `${props.width}px ${props.height}px` : 'cover')};
opacity: ${props => (props.isVisible ? 1 : 0)};
transition-duration: ${props => `${props.transitionDuration}ms`};
transition-property: opacity;
Expand All @@ -41,11 +43,16 @@ class LazyHero extends Component {
constructor() {
super();
this.state = {
backgroundPositionY: 'center',
backgroundDimensions: null,
heroDimensions: null,
image: null,
isInViewport: false,
position: 'center',
};
this.handlePositionChange = this.handlePositionChange.bind(this);
this.handleResize = this.handleResize.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.updatePosition = this.updatePosition.bind(this);
this.updateSize = this.updateSize.bind(this);
}

componentDidMount() {
Expand All @@ -55,48 +62,78 @@ class LazyHero extends Component {
image.src = this.props.imageSrc;
image.onload = () => {
this.setState({ image });
this.handlePositionChange();

if (this.props.parallaxOffset > 0) {
this.updateSize();
this.updatePosition();
}
};

if (this.props.parallaxSpeed > 0) {
window.addEventListener('scroll', this.handlePositionChange);
window.addEventListener('resize', this.handlePositionChange);
if (this.props.parallaxOffset > 0) {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize);
}
}

componentWillUnmount() {
if (this.props.parallaxSpeed > 0) {
window.removeEventListener('scroll', this.handlePositionChange);
window.removeEventListener('resize', this.handlePositionChange);
if (this.props.parallaxOffset > 0) {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
}
}

handlePositionChange() {
if (!this.state.image || !(this.props.parallaxSpeed > 0)) return;

window.requestAnimationFrame(() => {
const { image } = this.state;
const scrolled = window.pageYOffset;
const heroWidth = this.ref.offsetWidth;
const heroHeight = this.ref.offsetHeight;
const parallaxOffset = scrolled * this.props.parallaxSpeed;
// Image height when it's scaled up/down to have the same width as the hero component
const scaledHeight = image.height / image.width * heroWidth;
// Get the current image height based on the proportion
const actualImageHeight = scaledHeight > heroHeight ? scaledHeight : heroHeight;
// Calculate the BG position so that it's slightly under the center line
const position = (heroHeight / 3) - (actualImageHeight / 2) + parallaxOffset;
// Do not scroll above the image border
const finalPosition = position < 0 ? position : 0;
this.setState({ position: `${Math.round(finalPosition)}px` });
});
handleScroll() {
this.updatePosition();
}

render() {
const imageStyle = {
backgroundPosition: `center ${this.state.position}`,
handleResize() {
this.updateSize();
this.updatePosition();
}

updateSize() {
if (!this.state.image) return;

const heroDimensions = {
height: this.ref.offsetHeight,
width: this.ref.offsetWidth,
};

const imageDimensions = {
height: this.state.image.height,
width: this.state.image.width,
};

const resizedImage = resizeToCover(imageDimensions, heroDimensions);
const initialVisibleImageHeight = resizedImage.height - this.props.parallaxOffset;

const minHeight = initialVisibleImageHeight < heroDimensions.height
? resizedImage.height + heroDimensions.height - initialVisibleImageHeight
: resizedImage.height;

const finalHeight = minHeight + (this.ref.offsetTop * 2);

const backgroundDimensions = resizeToCover(imageDimensions, { height: finalHeight });
this.setState({ backgroundDimensions, heroDimensions });
}

updatePosition() {
if (!this.state.backgroundDimensions) return;
const position = 0
+ this.ref.offsetTop
// Center image vertically
- (this.state.backgroundDimensions.height / 2)
+ (this.state.heroDimensions.height / 2)
- (this.props.parallaxOffset / 2)
// Apply scroll position
+ (this.props.parallaxOffset * scrolledOverPercent(this.ref));

this.setState({ backgroundPositionY: `${Math.round(position)}px` });
}

render() {
const { backgroundDimensions, backgroundPositionY } = this.state;

return (
<Root
className={this.props.className}
Expand All @@ -105,19 +142,20 @@ class LazyHero extends Component {
style={this.props.style}
>
<Img
height={backgroundDimensions && backgroundDimensions.height}
isVisible={this.state.image && this.state.isInViewport}
isFixed={this.props.isFixed || this.props.parallaxSpeed > 0}
isFixed={this.props.isFixed || this.props.parallaxOffset > 0}
src={this.props.imageSrc}
style={imageStyle}
style={{ backgroundPositionY }}
transitionDuration={this.props.transitionDuration}
transitionTimingFunction={this.props.transitionTimingFunction}
width={backgroundDimensions && backgroundDimensions.width}
/>
<Overlay
color={this.props.color}
isCentered={this.props.isCentered}
opacity={this.props.opacity}
>

{this.props.children && <div>{this.props.children}</div>}
</Overlay>
</Root>
Expand All @@ -134,8 +172,7 @@ LazyHero.defaultProps = {
isFixed: false,
minHeight: '50vh',
opacity: 0.8,
parallaxSpeed: 0,
scrollIconColor: 'rgba(0, 0, 0, 0.4)',
parallaxOffset: 0,
style: undefined,
transitionDuration: 600,
transitionTimingFunction: 'ease-in-out',
Expand All @@ -150,7 +187,7 @@ LazyHero.propTypes = {
isFixed: PropTypes.bool,
minHeight: PropTypes.string,
opacity: PropTypes.number,
parallaxSpeed: PropTypes.number,
parallaxOffset: PropTypes.number,
style: PropTypes.object, // eslint-disable-line react/forbid-prop-types
transitionDuration: PropTypes.number,
transitionTimingFunction: PropTypes.string,
Expand Down
81 changes: 81 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Clamp a value between two other values
* @param {number} number
* @param {number} min
* @param {number} max
* @return {number}
*/
export function clamp(number, min = 0, max = 1) {
return Math.min(Math.max(number, min), max);
}

/*
* Get the percentage scrolled over an element
* @param {HTMLElement} element
* @return {number} value between 0 and 1
*/
export function scrolledOverPercent(element) {
const scrolled = window.pageYOffset;
const height = element.offsetHeight;
const top = element.offsetTop;
const percent = scrolled / (top + height);

return clamp(percent);
}

/*
* Resize to original aspect ratio
* @param {Object} dimensions The original dimensions
* @param {number} dimensions.height
* @param {number} dimensions.width
* @param {string} prop Property name
* @param {number} value Current property value
* @param {Object} new height and width
*/
export function resizeToRatio(dimensions, prop, value) {
const otherProp = prop === 'width' ? 'height' : 'width';
const otherPropValue = Math.round(value * dimensions[otherProp] / dimensions[prop]);

return {
[otherProp]: otherPropValue,
[prop]: value,
};
}

/*
* Simulate "background-position: contain"
* @param {Object} dimensions The original dimensions
* @param {number} dimensions.height
* @param {number} dimensions.width
* @param {Object} maxDimensions The available space
* @param {number} maxDimensions.height
* @param {number} maxDimensions.width
* @return {Object} new height and width
*/
export function resizeToContain(dimensions, maxDimensions) {
return Object.keys(dimensions).reduce((prevDimensions, prop) => (
prevDimensions[prop] > maxDimensions[prop]
? resizeToRatio(prevDimensions, prop, maxDimensions[prop])
: prevDimensions
), dimensions);
}

/*
* Simulate "background-position: cover"
* @param {Object} dimensions The original dimensions
* @param {number} dimensions.height
* @param {number} dimensions.width
* @param {Object} maxDimensions The available space
* @param {number} maxDimensions.height
* @param {number} maxDimensions.width
* @return {Object} new height and width
*/
export function resizeToCover(dimensions, maxDimensions) {
const dimensionsAfterContain = resizeToContain(dimensions, maxDimensions);

return Object.keys(dimensions).reduce((prevDimensions, prop) => (
prevDimensions[prop] < maxDimensions[prop]
? resizeToRatio(prevDimensions, prop, maxDimensions[prop])
: prevDimensions
), dimensionsAfterContain);
}
25 changes: 14 additions & 11 deletions website/src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ function App(props) {
key={props.id}
minHeight={props.knobs.minHeight.current}
opacity={props.knobs.opacity.current}
parallaxSpeed={props.knobs.parallaxSpeed.current}
parallaxOffset={parseInt(props.knobs.parallaxOffset.current, 10)}
style={{ overflow: 'hidden' }}
transitionDuration={props.knobs.transitionDuration.current}
transitionDuration={parseInt(props.knobs.transitionDuration.current, 10)}
transitionTimingFunction={props.knobs.transitionTimingFunction.current}
>
<Logo />
Expand Down Expand Up @@ -99,12 +99,12 @@ const enhance = compose(
step: 0.1,
type: 'number',
},
parallaxSpeed: {
current: 0.1,
parallaxOffset: {
current: 100,
default: 0,
description: 'Speed at which the parallax effect runs. 0 means that the effect is inactive.',
description: 'Offset that is added to the hero height so that a parallax effect is generated. 0 means that the effect is inactive.',
min: 0,
step: 0.1,
step: 50,
type: 'number',
},
style: {
Expand All @@ -128,12 +128,15 @@ const enhance = compose(
},
}),
withHandlers({
onKnobChange: ({ knobs, setKnobs }) =>
(name, current) => setKnobs({
...knobs,
[name]: { ...knobs[name], current },
}),
onReload: ({ setId }) => () => setId(Date.now()),
onKnobChange: ({ knobs, setId, setKnobs }) =>
(name, current) => {
setKnobs({
...knobs,
[name]: { ...knobs[name], current },
});
setId(Date.now());
},
}),
);

Expand Down

0 comments on commit bae62d6

Please sign in to comment.