From 543bb38b2aeffeddf34ce3b6aff644fb5dafa0fc Mon Sep 17 00:00:00 2001 From: jljsj33 Date: Fri, 15 Jun 2018 19:12:30 +0800 Subject: [PATCH] add animate demo (carousel3d, snow) --- exhibition/demo/carousel3d.md | 351 ++++++++++++++++++++++++++++++++++ exhibition/demo/snow.md | 280 +++++++++++++++++++++++++++ exhibition/js/carousel3d.css | 62 ++++++ exhibition/js/carousel3d.jsx | 268 ++++++++++++++++++++++++++ exhibition/js/money.less | 18 ++ exhibition/js/snow.css | 37 ++++ exhibition/js/snow.jsx | 221 +++++++++++++++++++++ 7 files changed, 1237 insertions(+) create mode 100644 exhibition/demo/carousel3d.md create mode 100644 exhibition/demo/snow.md create mode 100644 exhibition/js/carousel3d.css create mode 100644 exhibition/js/carousel3d.jsx create mode 100644 exhibition/js/money.less create mode 100644 exhibition/js/snow.css create mode 100644 exhibition/js/snow.jsx diff --git a/exhibition/demo/carousel3d.md b/exhibition/demo/carousel3d.md new file mode 100644 index 00000000..39206884 --- /dev/null +++ b/exhibition/demo/carousel3d.md @@ -0,0 +1,351 @@ +--- +order: 7 +chinese: 卡片旋转 +english: carousel +image: https://gw.alipayobjects.com/zos/rmsportal/HOmyKwEoPKktzFyEsKOG.jpg +--- + +carousel 3d 卡片的旋转效果。 + +--- + +支付宝客户端里的小钱袋产品的心愿卡片菜单,手机上的一种卡片的收纳方式。 + +> 模糊比较耗性能,手机上不建议开启。 + +```jsx +import PropTypes from 'prop-types'; + +// const currentDpr = window.devicePixelRatio; +// const defaultDpr = 2; // sketch 里用的是 iphone 6 尺寸; +const dpr = 0.5;// currentDpr / defaultDpr; + +class Carousel3d extends React.PureComponent { + static propTypes = { + children: PropTypes.any, + style: PropTypes.object, + className: PropTypes.string, + onChange: PropTypes.func, + tilt: PropTypes.string, + duration: PropTypes.string, + ease: PropTypes.string, + blurIncrease: PropTypes.number, + opacityDecline: PropTypes.number, + opacityBasics: PropTypes.number, + moveRange: PropTypes.number, + childMaxLength: PropTypes.number, + perspective: PropTypes.number, + z: PropTypes.number, + current: PropTypes.number, + } + static defaultProps = { + onChange: () => { }, + tilt: '5rem', + duration: '.45s', + ease: 'cubic-bezier(0.215, 0.61, 0.355, 1)', + blurIncrease: 8, + opacityDecline: 0.1, + opacityBasics: 0.5, + moveRange: 2, + childMaxLength: 6, + perspective: 2800, + z: 800, + current: 0, + }; + constructor(props) { + super(props); + this.setLengthAndAngle(props); + this.state = { + rotate: -props.current * this.angle, + current: props.current, + transition: 'none', + }; + } + componentDidMount() { + this.w = document.body.clientWidth; + window.addEventListener('mouseup', this.onTouchEnd); + } + componentWillReceiveProps(nextProps) { + const { current, children } = nextProps; + if ( + current !== this.state.current && current !== this.props.current + || React.Children.toArray(children).length !== React.Children + .toArray(this.props.children).length + ) { + this.setLengthAndAngle(nextProps); + this.setState({ + current: nextProps.current, + rotate: -nextProps.current * this.angle, + transition: `transform ${nextProps.duration} ${nextProps.ease}`, + }); + } + } + + onTouchStart = (e) => { + if (e.touches && e.touches.length > 1 || this.length <= 1) { + return; + } + this.startX = e.pageX || e.touches[0].pageX; + this.startRotate = Math.round(this.state.rotate / this.angle) * this.angle; // 偏移修复; + } + onTouchMove = (e) => { + if (e.touches && e.touches.length > 1 || this.length <= 1 || !this.startX) { + return; + } + const x = e.pageX || e.touches[0].pageX; + const differ = (x - this.startX) * this.props.moveRange; // 幅度加大; + const rotate = this.startRotate + differ / this.w * this.angle; + const r = (Math.abs(Math.ceil(this.state.rotate / 360)) * 360 - rotate) % 360; + const current = Math.round(r / this.angle) % this.length; + this.setState({ + rotate, + current, + transition: 'none', + }, () => { + this.props.onChange({ + current, + rotate, + eventType: 'move', + }); + }); + } + onTouchEnd = (e) => { + if (e.changedTouches && e.changedTouches.length > 1 || this.length <= 1 || !this.startX) { + return; + } + const x = e.pageX || e.changedTouches[0].pageX; + const differ = x - this.startX; + const { current, rotate } = this.state; + const n = differ > 0 ? 1 : -1; + const newRotate = this.startRotate + n * this.angle * + Math.round(Math.abs((rotate - this.startRotate) / this.angle)); + this.setState({ + rotate: newRotate, + transition: `transform ${this.props.duration} ${this.props.ease}`, + }, () => { + this.startX = null; + this.props.onChange({ + current, + rotate: newRotate, + eventType: 'end', + }); + }); + } + setLengthAndAngle = (props) => { + this.length = React.Children.toArray(props.children).length; + this.length = this.length > props.childMaxLength ? props.childMaxLength : this.length; + this.angle = 360 / this.length; + } + getAnimStyle = (n, length) => { + const { opacityBasics, opacityDecline, blurIncrease } = this.props; + const center = length / 2; + const i = n > center ? center * 2 - n : n; + let opacity = 1 - ((i - 1) * opacityDecline + opacityBasics * (n ? 1 : 0)); + opacity = opacity < 0.1 ? 0.1 : opacity; + const d = { + opacity, + }; + if (blurIncrease) { + d.filter = `blur(${i * blurIncrease}px)`; + } + return d; + } + getChildrenToRender = (children) => { + const { childMaxLength, z } = this.props; + const newChildren = React.Children.toArray(children); + const length = newChildren.length; + const zDpr = z * dpr; + return newChildren.map((item, i) => { + if (i >= childMaxLength) { + return null; + } + const transform = `rotateY(${this.angle * i}deg) translateZ(${zDpr}px) rotateY(-${this.angle * i}deg) `; + const animStyle = this.getAnimStyle( + Math.abs(this.state.current - i), + length > childMaxLength ? childMaxLength : length + ); + const style = { + transform, + // opacity: animStyle.opacity, 留坑,preserve-3d 不可以与 opacity 同时使用,排查了一下午 + }; + return ( +
+
+
+ {/* transform 与 filter 的距阵冲突,图层分离 */} +
+ {item} +
+
+
+
+ ); + }); + } + render() { + const { onChange, ...props } = this.props; + const { + children, tilt, style, z, perspective, + } = props; + const zDpr = z * dpr; + const perspectiveDpr = perspective * dpr; + const childrenToRender = this.getChildrenToRender(children, perspective); + [ + 'tilt', + 'duration', + 'ease', + 'blurIncrease', + 'opacityDecline', + 'opacityBasics', + 'moveRange', + 'childMaxLength', + 'perspective', + 'z', + 'current', + ].forEach(k => delete props[k]); + return ( +
+
+
+
+ {childrenToRender} +
+
+
+
+ ); + } +} + +const imgWrapper = [ + 'https://zos.alipayobjects.com/rmsportal/DGOtoWASeguMJgV.png', + 'https://zos.alipayobjects.com/rmsportal/PDiTkHViQNVHddN.png', + 'https://zos.alipayobjects.com/rmsportal/QJmGZYJBRLkxFSy.png', + 'https://zos.alipayobjects.com/rmsportal/pTfNdthdsUpLPLJ.png', + 'https://zos.alipayobjects.com/rmsportal/TDIbcrKdLWVeWJM.png', + 'https://zos.alipayobjects.com/rmsportal/dvQuFtUoRmvWLsZ.png', + /* 'https://zos.alipayobjects.com/rmsportal/QqWQKvgLSJaYbpr.png', + 'https://zos.alipayobjects.com/rmsportal/vJcpMCTaSKSVWyH.png', */ +]; +// 使用 Carousel3d 的代码 +function Carousel() { + const children = imgWrapper.map((src, i) => ( +
+ )); + return ( +
+ + {children} + +
); +} + + +ReactDOM.render( + +, mountNode); +``` + +```css +.carousel-demo-wrapper { + position: relative; + background: #3949C0; + overflow: hidden; + height: 500px; +} + +.carousel-demo { + width: 100%; + height: 100%; +} + +.carousel-wrapper { + position: absolute; + width: 250px; + height: 300px; + margin: auto; + left: 0; + right: 0; + top: 120px; + padding-top: 350px; + margin-top: -350px; +} + +.carousel { + position: relative; + width: 100%; + margin: auto; + height: 100%; +} + +.carouselContent { + transform-style: preserve-3d; + width: 100%; +} + +.itemWrapper { + position: absolute; + top: 0; + left: 0; + transform-style: preserve-3d; + width: 100%; +} + +.itemWrapper .rotateLayer .bgAndBlurLayer { + border-radius: 8px; + overflow: hidden; + background: #fff; + transition: filter .45s; + margin: auto; +} + +.itemWrapper .rotateLayer .bgAndBlurLayer .contentLayer { + transition: opacity .65s; +} + +.img-wrapper { + height: 300px; + background-size: cover; + background-position: center; +} +``` \ No newline at end of file diff --git a/exhibition/demo/snow.md b/exhibition/demo/snow.md new file mode 100644 index 00000000..3bd91d7b --- /dev/null +++ b/exhibition/demo/snow.md @@ -0,0 +1,280 @@ +--- +order: 6 +chinese: 掉落效果 +english: snow +image: https://gw.alipayobjects.com/zos/rmsportal/CdkOMmURLntHGuuqFXnj.jpg +--- + +元素从上往下掉落的一个效果。 + +--- + +支付宝客户端里的小钱袋产品的金钱发生变化时的金币掉落效果,重看点刷新按钮。 + +[money.less 请查看这里](https://github.com/ant-design/ant-motion/tree/master/exhibition/js/money.less); + +```jsx + +import TweenOne from 'rc-tween-one'; +import BezierPlugin from 'rc-tween-one/lib/plugin/BezierPlugin'; +import PropTypes from 'prop-types'; + +import '../js/money.less'; + +TweenOne.plugins.push(BezierPlugin); + +class Snow extends React.Component { + static propTypes = { + children: PropTypes.any, + className: PropTypes.string, + prefixCls: PropTypes.string, + amount: PropTypes.number, + repeat: PropTypes.number, + ease: PropTypes.string, + startArea: PropTypes.object, + endArea: PropTypes.object, + startDelayRandom: PropTypes.number, + basicToDuration: PropTypes.number, + randomToDuration: PropTypes.number, + rotateRandom: PropTypes.number, + bezierSegmentation: PropTypes.number, + onEnd: PropTypes.func, + } + static defaultProps = { + prefixCls: 'snow', + amount: 10, + repeat: 0, + ease: 'linear', + startArea: { + x: 0, y: -200, width: '100%', height: 50, + }, + endArea: { + x: -200, y: '100%', width: '120%', height: 100, + }, + basicToDuration: 1200, + randomToDuration: 800, + startDelayRandom: 800, + rotateRandom: 180, + bezierSegmentation: 2, + onEnd: () => { }, + }; + + constructor(props) { + super(props); + this.state = { + children: null, + }; + } + componentDidMount() { + this.setChilrenToState(); + } + + onAnimEnd = () => { + this.animEnd += 1; + if (this.animEnd >= this.props.amount) { + this.animEnd = 0; + if (this.props.onEnd) { + this.props.onEnd(); + } + } + } + + setChilrenToState() { + const children = this.getChildrenToRender(); + this.setState({ + children, + }); + } + + getChildrenToRender = () => { + const { + bezierSegmentation, basicToDuration, randomToDuration, + amount, ease, startDelayRandom, repeat, rotateRandom, + } = this.props; + const children = React.Children.toArray(this.props.children); + const rect = this.wrapperDom.getBoundingClientRect(); + const startArea = this.dataToNumber(this.props.startArea, rect); + const endArea = this.dataToNumber(this.props.endArea, rect); + return Array(amount).fill(1).map((k, i) => { + const item = children[Math.floor(Math.random() * children.length)]; + const vars = Array(bezierSegmentation).fill(1).map((c, j) => { + const hegiht = endArea.y - startArea.y - startArea.height; + const y = (hegiht / bezierSegmentation) * (j + 1); + const x = Math.random() * (Math.max(startArea.width, endArea.width) + + Math.min(startArea.x, endArea.x)); + // console.log(hegiht, startArea, endArea, y); + return { + y, + x, + }; + }); + const delay = Math.random() * startDelayRandom; + const animation = { + bezier: { + type: 'soft', + autRotate: true, + vars, + }, + ease, + repeat, + repeatDelay: delay, + delay, + duration: basicToDuration + Math.random() * randomToDuration, + onComplete: this.onAnimEnd, + }; + const style = { + transform: `translate(${Math.random() * (startArea.width) + startArea.x}px, ${ + Math.random() * (startArea.height) + startArea.y + }px)`, + }; + const child = rotateRandom ? ( + + {item} + + ) : item; + return ( + + {child} + + ); + }); + } + dataToNumber = (obj, rect) => { + const toNumber = (v, full) => { + if (typeof v === 'number') { + return v; + } + const unit = v.replace(/[0-9|.]/g, ''); + switch (unit) { + case '%': + return parseFloat(v) * full / 100; + case 'em': + return parseFloat(v) * 16; + default: + return null; + } + }; + return { + x: toNumber(obj.x, rect.width), + y: toNumber(obj.y, rect.height), + width: toNumber(obj.width, rect.width), + height: toNumber(obj.height, rect.height), + }; + } + animEnd = 0; + render() { + const { prefixCls, ...props } = this.props; + const { children } = this.state; + [ + 'amount', + 'repeat', + 'ease', + 'startArea', + 'endArea', + 'basicToDuration', + 'randomToDuration', + 'startDelayRandom', + 'bezierSegmentation', + 'rotateRandom', + 'onEnd', + ].forEach(k => delete props[k]); + const className = `${prefixCls}${props.className ? ` ${props.className}` : ''}`; + return ( +
{ + this.wrapperDom = c; + }} + className={className} + > + {children} +
+ ); + } +} + +class SnowDemo extends React.Component { + state = { + show: true, + } + onEnd = () => { + this.setState({ + show: false, + }); + } + render() { + const children = Array(5).fill(1).map((c, i) => ( +
+ )); + return ( +
+
+ {this.state.show && ( + + {children} + + )} +
+
+ ); + } +} + +ReactDOM.render( + +, mountNode); +``` + +```css +.snow-demo-wrapper { + background: #DFEAFF; + overflow: hidden; + height: 500px; + display: flex; + align-items: center; + position: relative; +} + +.snow-demo { + width: 300px; + height: 90%; + margin: auto; + position: relative; + background-image: url(https://gw.alipayobjects.com/zos/rmsportal/dNpuKMDHFEpMGrTxdLVR.jpg); + background-position: top; + background-size: 100% auto; + box-shadow: 0 0 32px rgba(0, 0, 0, 0.15); +} + +.snow { + width: 100%; + height: 100%; + position: absolute; + top: 0; + overflow: hidden; +} + +.snowChild { + position: absolute; + top: 0; + left: 0; +} + +.snowRotate { + transform-origin: center center; +} +``` \ No newline at end of file diff --git a/exhibition/js/carousel3d.css b/exhibition/js/carousel3d.css new file mode 100644 index 00000000..8897c1ce --- /dev/null +++ b/exhibition/js/carousel3d.css @@ -0,0 +1,62 @@ +.carousel-demo-wrapper { + position: relative; + background: #3949C0; + background: linear-gradient(45deg, #1e5799 0%,#3949c0 0%,#4e7de4 100%); + overflow: hidden; + height: 500px; +} + +.carousel-demo { + width: 100%; + height: 100%; +} + +.carousel-wrapper { + position: absolute; + width: 250px; + height: 300px; + margin: auto; + left: 0; + right: 0; + top: 120px; + padding-top: 350px; + margin-top: -350px; +} + +.carousel { + position: relative; + width: 100%; + margin: auto; + height: 100%; +} + +.carouselContent { + transform-style: preserve-3d; + width: 100%; +} + +.itemWrapper { + position: absolute; + top: 0; + left: 0; + transform-style: preserve-3d; + width: 100%; +} + +.itemWrapper .rotateLayer .bgAndBlurLayer { + border-radius: 8px; + overflow: hidden; + background: #fff; + transition: filter .45s; + margin: auto; +} + +.itemWrapper .rotateLayer .bgAndBlurLayer .contentLayer { + transition: opacity .65s; +} + +.img-wrapper { + height: 300px; + background-size: cover; + background-position: center; +} \ No newline at end of file diff --git a/exhibition/js/carousel3d.jsx b/exhibition/js/carousel3d.jsx new file mode 100644 index 00000000..36d5475e --- /dev/null +++ b/exhibition/js/carousel3d.jsx @@ -0,0 +1,268 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './carousel3d.css'; + +// const currentDpr = window.devicePixelRatio; +// const defaultDpr = 2; // sketch 里用的是 iphone 6 尺寸; +const dpr = 0.5;// currentDpr / defaultDpr; + +class Carousel3d extends React.PureComponent { + static propTypes = { + children: PropTypes.any, + style: PropTypes.object, + className: PropTypes.string, + onChange: PropTypes.func, + tilt: PropTypes.string, + duration: PropTypes.string, + ease: PropTypes.string, + blurIncrease: PropTypes.number, + opacityDecline: PropTypes.number, + opacityBasics: PropTypes.number, + moveRange: PropTypes.number, + childMaxLength: PropTypes.number, + perspective: PropTypes.number, + z: PropTypes.number, + current: PropTypes.number, + } + static defaultProps = { + onChange: () => { }, + tilt: '5rem', + duration: '.45s', + ease: 'cubic-bezier(0.215, 0.61, 0.355, 1)', + blurIncrease: 8, + opacityDecline: 0.1, + opacityBasics: 0.5, + moveRange: 2, + childMaxLength: 6, + perspective: 2800, + z: 800, + current: 0, + }; + constructor(props) { + super(props); + this.setLengthAndAngle(props); + this.state = { + rotate: -props.current * this.angle, + current: props.current, + transition: 'none', + }; + } + componentDidMount() { + this.w = document.body.clientWidth; + window.addEventListener('mouseup', this.onTouchEnd); + } + componentWillReceiveProps(nextProps) { + const { current, children } = nextProps; + if ( + current !== this.state.current && current !== this.props.current + || React.Children.toArray(children).length !== React.Children + .toArray(this.props.children).length + ) { + this.setLengthAndAngle(nextProps); + this.setState({ + current: nextProps.current, + rotate: -nextProps.current * this.angle, + transition: `transform ${nextProps.duration} ${nextProps.ease}`, + }); + } + } + + onTouchStart = (e) => { + if (e.touches && e.touches.length > 1 || this.length <= 1) { + return; + } + this.startX = e.pageX || e.touches[0].pageX; + this.startRotate = Math.round(this.state.rotate / this.angle) * this.angle; // 偏移修复; + } + onTouchMove = (e) => { + if (e.touches && e.touches.length > 1 || this.length <= 1 || !this.startX) { + return; + } + const x = e.pageX || e.touches[0].pageX; + const differ = (x - this.startX) * this.props.moveRange; // 幅度加大; + const rotate = this.startRotate + differ / this.w * this.angle; + const r = (Math.abs(Math.ceil(this.state.rotate / 360)) * 360 - rotate) % 360; + const current = Math.round(r / this.angle) % this.length; + this.setState({ + rotate, + current, + transition: 'none', + }, () => { + this.props.onChange({ + current, + rotate, + eventType: 'move', + }); + }); + } + onTouchEnd = (e) => { + if (e.changedTouches && e.changedTouches.length > 1 || this.length <= 1 || !this.startX) { + return; + } + const x = e.pageX || e.changedTouches[0].pageX; + const differ = x - this.startX; + const { current, rotate } = this.state; + const n = differ > 0 ? 1 : -1; + const newRotate = this.startRotate + n * this.angle * + Math.round(Math.abs((rotate - this.startRotate) / this.angle)); + this.setState({ + rotate: newRotate, + transition: `transform ${this.props.duration} ${this.props.ease}`, + }, () => { + this.startX = null; + this.props.onChange({ + current, + rotate: newRotate, + eventType: 'end', + }); + }); + } + setLengthAndAngle = (props) => { + this.length = React.Children.toArray(props.children).length; + this.length = this.length > props.childMaxLength ? props.childMaxLength : this.length; + this.angle = 360 / this.length; + } + getAnimStyle = (n, length) => { + const { opacityBasics, opacityDecline, blurIncrease } = this.props; + const center = length / 2; + const i = n > center ? center * 2 - n : n; + let opacity = 1 - ((i - 1) * opacityDecline + opacityBasics * (n ? 1 : 0)); + opacity = opacity < 0.1 ? 0.1 : opacity; + const d = { + opacity, + }; + if (blurIncrease) { + d.filter = `blur(${i * blurIncrease}px)`; + } + return d; + } + getChildrenToRender = (children) => { + const { childMaxLength, z } = this.props; + const newChildren = React.Children.toArray(children); + const length = newChildren.length; + const zDpr = z * dpr; + return newChildren.map((item, i) => { + if (i >= childMaxLength) { + return null; + } + const transform = `rotateY(${this.angle * i}deg) translateZ(${zDpr}px) rotateY(-${this.angle * i}deg) `; + const animStyle = this.getAnimStyle( + Math.abs(this.state.current - i), + length > childMaxLength ? childMaxLength : length + ); + const style = { + transform, + // opacity: animStyle.opacity, 留坑,preserve-3d 不可以与 opacity 同时使用,排查了一下午 + }; + return ( +
+
+
+ {/* transform 与 filter 的距阵冲突,图层分离 */} +
+ {item} +
+
+
+
+ ); + }); + } + render() { + const { onChange, ...props } = this.props; + const { + children, tilt, style, z, perspective, + } = props; + const zDpr = z * dpr; + const perspectiveDpr = perspective * dpr; + const childrenToRender = this.getChildrenToRender(children, perspective); + [ + 'tilt', + 'duration', + 'ease', + 'blurIncrease', + 'opacityDecline', + 'opacityBasics', + 'moveRange', + 'childMaxLength', + 'perspective', + 'z', + 'current', + ].forEach(k => delete props[k]); + return ( +
+
+
+
+ {childrenToRender} +
+
+
+
+ ); + } +} + +const imgWrapper = [ + 'https://zos.alipayobjects.com/rmsportal/DGOtoWASeguMJgV.png', + 'https://zos.alipayobjects.com/rmsportal/PDiTkHViQNVHddN.png', + 'https://zos.alipayobjects.com/rmsportal/QJmGZYJBRLkxFSy.png', + 'https://zos.alipayobjects.com/rmsportal/pTfNdthdsUpLPLJ.png', + 'https://zos.alipayobjects.com/rmsportal/TDIbcrKdLWVeWJM.png', + 'https://zos.alipayobjects.com/rmsportal/dvQuFtUoRmvWLsZ.png', + /* 'https://zos.alipayobjects.com/rmsportal/QqWQKvgLSJaYbpr.png', + 'https://zos.alipayobjects.com/rmsportal/vJcpMCTaSKSVWyH.png', */ +]; + +export default function Carousel() { + const children = imgWrapper.map((src, i) => ( +
+ )); + return ( +
+ + {children} + +
); +} + diff --git a/exhibition/js/money.less b/exhibition/js/money.less new file mode 100644 index 00000000..f06787bd --- /dev/null +++ b/exhibition/js/money.less @@ -0,0 +1,18 @@ +.addMoneyAnim { + background: url('https://gw.alipayobjects.com/zos/rmsportal/fseEOKMDOXOieJiHKuQg.png') no-repeat; + background-size: 420px 104px; + width: 52px; + height: 52px; + animation: rotateMoney 0.45s step-start infinite; +} + +.addMoneyFrame(@one, @frame, @i: 0, @name: 0%) when (@i =< @frame) { + .addMoneyFrame(@one, @frame, @i + 1, (100% / @frame * (@i + 1))); + @{name} { + background-position: -(mod(@i, @one) * 52px) -(floor(@i / @one)*52px); + } +} + +@keyframes rotateMoney { + .addMoneyFrame(8, 15); +} \ No newline at end of file diff --git a/exhibition/js/snow.css b/exhibition/js/snow.css new file mode 100644 index 00000000..6e4f99e3 --- /dev/null +++ b/exhibition/js/snow.css @@ -0,0 +1,37 @@ +.snow-demo-wrapper { + background: #4777E3; + overflow: hidden; + height: 500px; + display: flex; + align-items: center; + position: relative; +} + +.snow-demo { + width: 300px; + height: 90%; + margin: auto; + position: relative; + background-image: url(https://gw.alipayobjects.com/zos/rmsportal/dNpuKMDHFEpMGrTxdLVR.jpg); + background-position: top; + background-size: 100% auto; + box-shadow: 0 0 32px rgba(0, 0, 0, 0.15); +} + +.snow { + width: 100%; + height: 100%; + position: absolute; + top: 0; + overflow: hidden; +} + +.snowChild { + position: absolute; + top: 0; + left: 0; +} + +.snowRotate { + transform-origin: center center; +} \ No newline at end of file diff --git a/exhibition/js/snow.jsx b/exhibition/js/snow.jsx new file mode 100644 index 00000000..7fcf6af9 --- /dev/null +++ b/exhibition/js/snow.jsx @@ -0,0 +1,221 @@ + +import TweenOne from 'rc-tween-one'; +import BezierPlugin from 'rc-tween-one/lib/plugin/BezierPlugin'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import './snow.css'; +import './money.less'; + +TweenOne.plugins.push(BezierPlugin); + +class Snow extends React.Component { + static propTypes = { + children: PropTypes.any, + className: PropTypes.string, + prefixCls: PropTypes.string, + amount: PropTypes.number, + repeat: PropTypes.number, + ease: PropTypes.string, + startArea: PropTypes.object, + endArea: PropTypes.object, + startDelayRandom: PropTypes.number, + basicToDuration: PropTypes.number, + randomToDuration: PropTypes.number, + rotateRandom: PropTypes.number, + bezierSegmentation: PropTypes.number, + onEnd: PropTypes.func, + } + static defaultProps = { + prefixCls: 'snow', + amount: 10, + repeat: 0, + ease: 'linear', + startArea: { + x: 0, y: -200, width: '100%', height: 50, + }, + endArea: { + x: -200, y: '100%', width: '120%', height: 100, + }, + basicToDuration: 1200, + randomToDuration: 800, + startDelayRandom: 800, + rotateRandom: 180, + bezierSegmentation: 2, + onEnd: () => { }, + }; + + constructor(props) { + super(props); + this.state = { + children: null, + }; + } + componentDidMount() { + this.setChilrenToState(); + } + + onAnimEnd = () => { + this.animEnd += 1; + if (this.animEnd >= this.props.amount) { + this.animEnd = 0; + if (this.props.onEnd) { + this.props.onEnd(); + } + } + } + + setChilrenToState() { + const children = this.getChildrenToRender(); + this.setState({ + children, + }); + } + + getChildrenToRender = () => { + const { + bezierSegmentation, basicToDuration, randomToDuration, + amount, ease, startDelayRandom, repeat, rotateRandom, + } = this.props; + const children = React.Children.toArray(this.props.children); + const rect = this.wrapperDom.getBoundingClientRect(); + const startArea = this.dataToNumber(this.props.startArea, rect); + const endArea = this.dataToNumber(this.props.endArea, rect); + return Array(amount).fill(1).map((k, i) => { + const item = children[Math.floor(Math.random() * children.length)]; + const vars = Array(bezierSegmentation).fill(1).map((c, j) => { + const hegiht = endArea.y - startArea.y - startArea.height; + const y = (hegiht / bezierSegmentation) * (j + 1); + const x = Math.random() * (Math.max(startArea.width, endArea.width) + + Math.min(startArea.x, endArea.x)); + // console.log(hegiht, startArea, endArea, y); + return { + y, + x, + }; + }); + const delay = Math.random() * startDelayRandom; + const animation = { + bezier: { + type: 'soft', + autRotate: true, + vars, + }, + ease, + repeat, + repeatDelay: delay, + delay, + duration: basicToDuration + Math.random() * randomToDuration, + onComplete: this.onAnimEnd, + }; + const style = { + transform: `translate(${Math.random() * (startArea.width) + startArea.x}px, ${ + Math.random() * (startArea.height) + startArea.y + }px)`, + }; + const child = rotateRandom ? ( + + {item} + + ) : item; + return ( + + {child} + + ); + }); + } + dataToNumber = (obj, rect) => { + const toNumber = (v, full) => { + if (typeof v === 'number') { + return v; + } + const unit = v.replace(/[0-9|.]/g, ''); + switch (unit) { + case '%': + return parseFloat(v) * full / 100; + case 'em': + return parseFloat(v) * 16; + default: + return null; + } + }; + return { + x: toNumber(obj.x, rect.width), + y: toNumber(obj.y, rect.height), + width: toNumber(obj.width, rect.width), + height: toNumber(obj.height, rect.height), + }; + } + animEnd = 0; + render() { + const { prefixCls, ...props } = this.props; + const { children } = this.state; + [ + 'amount', + 'repeat', + 'ease', + 'startArea', + 'endArea', + 'basicToDuration', + 'randomToDuration', + 'startDelayRandom', + 'bezierSegmentation', + 'rotateRandom', + 'onEnd', + ].forEach(k => delete props[k]); + const className = `${prefixCls}${props.className ? ` ${props.className}` : ''}`; + return ( +
{ + this.wrapperDom = c; + }} + className={className} + > + {children} +
+ ); + } +} + +export default class SnowDemo extends React.Component { + state = { + show: true, + } + onEnd = () => { + this.setState({ + show: false, + }); + } + render() { + const children = Array(5).fill(1).map((c, i) => ( +
+ )); + return ( +
+
+ {this.state.show && ( + + {children} + + )} +
+
+ ); + } +}