diff --git a/public/images/resume/article/banner.jpg b/public/images/resume/article/banner.jpg index e74601d..21843c3 100644 Binary files a/public/images/resume/article/banner.jpg and b/public/images/resume/article/banner.jpg differ diff --git a/public/images/resume/experiences/banner.jpg b/public/images/resume/experiences/banner.jpg new file mode 100644 index 0000000..8324336 Binary files /dev/null and b/public/images/resume/experiences/banner.jpg differ diff --git a/public/images/resume/projects/banner.jpg b/public/images/resume/projects/banner.jpg new file mode 100644 index 0000000..e74601d Binary files /dev/null and b/public/images/resume/projects/banner.jpg differ diff --git a/src/app/components.less b/src/app/components.less index e98d446..8c12e72 100644 --- a/src/app/components.less +++ b/src/app/components.less @@ -10,6 +10,7 @@ @import '../components/Resume/ResumeIntroduction/index.less'; @import '../components/Resume/ResumeProject/index.less'; @import '../components/Resume/ResumeSkill/index.less'; +@import '../components/CarouselThreeD/index.less'; @slider-menus: index introduction skill experience project article; @slider-menus-position-y: -670px -1110px -1330px -450px -890px -10px; diff --git a/src/components/Carousel3d/index.tsx b/src/components/Carousel3d/index.tsx index e665e14..5e5b9a6 100644 --- a/src/components/Carousel3d/index.tsx +++ b/src/components/Carousel3d/index.tsx @@ -66,6 +66,7 @@ export default function Carousel3d({ // 旋转值 const [rotate, setRotate] = useState(-defaultCurrent * angle.current); + const currentRotate = useRef(-defaultCurrent * angle.current); // 偏移值 const [transition, setTransition] = useState("none"); // 当前选择的节点 @@ -86,6 +87,7 @@ export default function Carousel3d({ angle.current = 360 / childrenLength.current; setCurrent(defaultCurrent); setRotate(-defaultCurrent * angle.current); + currentRotate.current = -defaultCurrent * angle.current; }, [childMaxLength, children, defaultCurrent]); useEffect(() => { @@ -116,6 +118,7 @@ export default function Carousel3d({ const current = Math.round(r / angle.current) % childrenLength.current; setRotate(rotate); + currentRotate.current = rotate; setCurrent(current); setTransition("none"); @@ -144,7 +147,12 @@ export default function Carousel3d({ angle.current * Math.round(Math.abs((rotate - startRotate.current) / angle.current)); + // const r = (Math.abs(Math.ceil(newRotate / 360)) * 360 - newRotate) % 360; + // const current = Math.round(r / angle.current) % childrenLength.current; + setRotate(newRotate); + currentRotate.current = newRotate; + // setCurrent(current); setTransition(`transform ${duration} ${ease}`); startX.current = 0; onChange({ @@ -184,23 +192,27 @@ export default function Carousel3d({ const center = (childMaxLength ?? length) / 2; const index = diffPosition > center ? center * 2 - diffPosition : diffPosition; - let opacity = + const opacity = Math.max( + 0.1, 1 - - ((index - 1) * opacityDecline + opacityBasics * (diffPosition ? 1 : 0)); - opacity = opacity < 0.1 ? 0.1 : opacity; + ((index - 1) * opacityDecline + + opacityBasics * (diffPosition ? 1 : 0)) + ); const animStyle: CSSProperties = { opacity, }; if (blurIncrease) { animStyle.filter = `blur(${index * blurIncrease}px)`; } - const style: CSSProperties = { - transformStyle: "preserve-3d", - transform, - // opacity: animStyle.opacity, 留坑,preserve-3d 不可以与 opacity 同时使用,排查了一下午 - }; return ( -
+
{/* transform 与 filter 的距阵冲突,图层分离 */} -
+
{item}
@@ -237,7 +246,7 @@ export default function Carousel3d({ [className!]: className, })} > -
+
; + className: string; + onChange: (arg: { + current: number; + rotate: number; + eventType: string; + }) => void; + tilt: string; + duration: string; + ease: string; + blurIncrease: number; + opacityDecline: number; + opacityBasics: number; + moveRange: number; + childMaxLength: number; + perspective: number; + z: number; + defaultCurrent: number; +} // 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: '15rem', - 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); - } - componentDidUpdate(prevProps) { - if (prevProps !== this.props) { - const { current, children } = this.props; +const DefaultWidth = 360; + +export default function Carousel3d({ + onChange = () => {}, + className, + tilt = "15rem", + 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, + defaultCurrent = 0, + children, + style = {}, +}: Partial) { + const zDpr = useMemo(() => z * dpr, [z]); + const perspectiveDpr = useMemo( + () => perspective * dpr, + [perspective] + ); + + // 子元素长度 + const childrenLength = useMemo( + () => Math.max(React.Children.toArray(children).length, childMaxLength), + [childMaxLength, children] + ); + // 偏移量 + const angle = useMemo( + () => DefaultWidth / childrenLength, + [childrenLength] + ); + // body的长度 + const clientWidth = useRef(0); + + // 旋转值 + const [rotate, setRotate] = useState(-defaultCurrent * angle); + // 偏移值 + const [transition, setTransition] = useState("none"); + // 当前选择的节点 + const [current, setCurrent] = useState(defaultCurrent); + + const startX = useRef(0); + + const startRotate = useMemo( + () => Math.round(rotate / angle) * angle, + [rotate, angle] + ); + + useEffect(() => { + clientWidth.current = document.body.clientWidth; + }, []); + + useEffect(() => { + setCurrent(defaultCurrent); + setRotate(-defaultCurrent * angle); + }, [angle, defaultCurrent]); + + useEffect(() => { + setTransition(`transform ${duration} ${ease}`); + }, [duration, ease]); + + const onChangeEvent = useCallback( + ( + eventType: "move" | "end", + data: { + current?: number; + rotate?: number; + } + ) => { + onChange({ + current, + rotate, + eventType, + ...data, + }); + }, + [current, onChange, rotate] + ); + + const onTouchStart = useCallback( + (e: MouseEvent & TouchEvent) => { + if ((e.touches && e.touches.length > 1) || childrenLength <= 1) { + return; + } + startX.current = e.pageX || e.touches[0].pageX; + }, + [childrenLength] + ); + + const onTouchMove = useCallback( + (e: MouseEvent & TouchEvent) => { if ( - (current !== this.state.current && current !== prevProps.current) || - React.Children.toArray(children).length !== - React.Children.toArray(prevProps.children).length + (e.touches && e.touches.length > 1) || + childrenLength <= 1 || + !startX.current ) { - this.setLengthAndAngle(this.props); - // eslint-disable-next-line - this.setState({ - current: this.props.current, - rotate: -this.props.current * this.angle, - transition: `transform ${this.props.duration} ${this.props.ease}`, - }); + return; } - } - } + const x = e.pageX || e.touches[0].pageX; + const differ = (x - startX.current) * moveRange; // 幅度加大; + const rotate = startRotate + (differ / clientWidth.current) * angle; + const r = + (Math.abs(Math.ceil(rotate / DefaultWidth)) * DefaultWidth - rotate) % + DefaultWidth; + const current = Math.round(r / angle) % childrenLength; - 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, + console.log("onTouchMove", current); + + setRotate(rotate); + setCurrent(current); + setTransition("none"); + + onChangeEvent("move", { 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, + }); + }, + [angle, childrenLength, moveRange, onChangeEvent, startRotate] + ); + + const onTouchEnd = useCallback( + (e: MouseEvent & TouchEvent) => { + if ( + (e.changedTouches && e.changedTouches.length > 1) || + childrenLength <= 1 || + !startX.current + ) { + return; + } + const x = e.pageX || e.changedTouches[0].pageX; + const differ = x - startX.current; + const n = differ > 0 ? 1 : -1; + const newRotate = + startRotate + + n * angle * Math.round(Math.abs((rotate - startRotate) / angle)); + + setRotate(newRotate); + + startX.current = 0; + onChangeEvent("end", { 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, + }); + }, + [angle, childrenLength, onChangeEvent, rotate, startRotate] + ); + + useEffect(() => { + window.addEventListener("mouseup", onTouchEnd as any); + + return () => { + window.removeEventListener("mouseup", onTouchEnd as any); }; - if (blurIncrease) { - d.filter = `blur(${i * blurIncrease}px)`; - } - return d; - }; - getChildrenToRender = (children) => { - const { childMaxLength, z } = this.props; - const newChildren = React.Children.toArray(children); + }, [onTouchEnd]); + + /** + * 渲染子元素 + */ + const renderChildren = () => { + const newChildren = React.Children.toArray(children) as ReactElement[]; const length = newChildren.length; const zDpr = z * dpr; return newChildren.map((item, i) => { @@ -168,38 +206,44 @@ class Carousel3d extends React.PureComponent { 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 同时使用,排查了一下午 + angle * i + }deg) translateZ(${zDpr}px) rotateY(-${angle * i}deg) `; + + const diffPosition = Math.abs(current - i); + + const center = Math.min(childMaxLength, length) / 2; + const index = + diffPosition > center ? center * 2 - diffPosition : diffPosition; + let opacity = + 1 - + ((index - 1) * opacityDecline + opacityBasics * (diffPosition ? 1 : 0)); + opacity = opacity < 0.1 ? 0.1 : opacity; + const animStyle: CSSProperties = { + opacity, }; + if (blurIncrease) { + animStyle.filter = `blur(${index * blurIncrease}px)`; + } return (
- {/* transform 与 filter 的距阵冲突,图层分离 */} -
+
{item}
@@ -208,60 +252,42 @@ class Carousel3d extends React.PureComponent { ); }); }; - 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 ( -
-
+ + return ( +
+
+
-
- {childrenToRender} -
+ {renderChildren()}
- ); - } +
+ ); } - -export default Carousel3d; diff --git a/src/components/Carousel3d/index.less b/src/components/CarouselThreeD/index.less similarity index 84% rename from src/components/Carousel3d/index.less rename to src/components/CarouselThreeD/index.less index 75760d8..818a20b 100644 --- a/src/components/Carousel3d/index.less +++ b/src/components/CarouselThreeD/index.less @@ -1,3 +1,11 @@ +.carouselDemoWrapper { + position: relative; + /* background: #3949C0; */ + background: rgba(0, 0, 0, 0.4); + overflow: hidden; + height: 380px; +} + .carouselDemo { width: 100%; height: 100%; diff --git a/src/components/CarouselThreeD/index.tsx b/src/components/CarouselThreeD/index.tsx new file mode 100644 index 0000000..eb2f772 --- /dev/null +++ b/src/components/CarouselThreeD/index.tsx @@ -0,0 +1,270 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck + +import PropTypes from "prop-types"; +import React from "react"; + +// const currentDpr = window.devicePixelRatio; +// const defaultDpr = 2; // sketch 里用的是 iphone 6 尺寸; +const dpr = 0.5; // currentDpr / defaultDpr; + +class CarouselThreeD 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: "15rem", + 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); + } + componentDidUpdate(prevProps) { + if (prevProps !== this.props) { + const { current, children, duration, ease } = this.props; + if ( + (current !== this.state.current && current !== prevProps.current) || + React.Children.toArray(children).length !== + React.Children.toArray(prevProps.children).length + ) { + this.setLengthAndAngle(this.props); + this.setState({ + current: current, + rotate: -current * this.angle, + transition: `transform ${duration} ${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); + for (const k of [ + "tilt", + "duration", + "ease", + "blurIncrease", + "opacityDecline", + "opacityBasics", + "moveRange", + "childMaxLength", + "perspective", + "z", + "current", + ]) + delete props[k]; + return ( +
+
+
+
+ {childrenToRender} +
+
+
+
+ ); + } +} + +export default CarouselThreeD; diff --git a/src/components/Resume/ResumeArticle/index.tsx b/src/components/Resume/ResumeArticle/index.tsx index 1bdd8c9..c638327 100644 --- a/src/components/Resume/ResumeArticle/index.tsx +++ b/src/components/Resume/ResumeArticle/index.tsx @@ -89,7 +89,7 @@ export default function ResumeArticle() {
- {!isMobile && ( +
    {} : onCubeMouseMove} + onMouseLeave={isMobile ? () => {} : onCubeMouseLeave} >
  • Q
- )} +
); diff --git a/src/components/Resume/ResumeExperience/index.tsx b/src/components/Resume/ResumeExperience/index.tsx index e6ae681..02b1302 100644 --- a/src/components/Resume/ResumeExperience/index.tsx +++ b/src/components/Resume/ResumeExperience/index.tsx @@ -131,39 +131,62 @@ export default function ResumeExperience() { }; return ( -
-
-

工作经历

+
+
+
- {isMobile ? ( - - {renderExperiences()} - - ) : ( - - {renderExperiences()} - + className={classNames( + "relative z-10 flex h-full flex-col items-center justify-center", + { + "!w-full": isMobile, + } )} + > +
+

工作经历

+
+ +
+ {isMobile ? ( + + {renderExperiences()} + + ) : ( + + {renderExperiences()} + + )} +
); diff --git a/src/components/Resume/ResumeIndex/index.less b/src/components/Resume/ResumeIndex/index.less index ba77537..33ad233 100644 --- a/src/components/Resume/ResumeIndex/index.less +++ b/src/components/Resume/ResumeIndex/index.less @@ -8,7 +8,7 @@ @lineWidth: 130px; // 椭圆x轴半径(长半径) @lineHeight: 45px; // 椭圆y轴半径(短半径) @pointRadius: 8px; // 球半径 - @count: 150; // 坐标点的数目(数目越大,动画越精细) + @count: 100; // 坐标点的数目(数目越大,动画越精细) .rotating-ellipse { width: @lineWidth * 2; @@ -19,6 +19,10 @@ top: @lineHeight - @pointRadius - 2px; width: @pointRadius * 2; height: @pointRadius * 2; + + @translateX: @lineWidth * cos(360deg); + @translateY: @lineHeight * sin(360deg); + transform: translate(@translateX, @translateY); } @keyframes rotate-spin { diff --git a/src/components/Resume/ResumeProject/index.tsx b/src/components/Resume/ResumeProject/index.tsx index 86de65f..d1a7200 100644 --- a/src/components/Resume/ResumeProject/index.tsx +++ b/src/components/Resume/ResumeProject/index.tsx @@ -10,6 +10,7 @@ import React, { useState } from "react"; import { Carousel, MessageModal, QrcodeModal } from "@/components"; import Carousel3d from "@/components/Carousel3d"; +import CarouselThreeD from "@/components/CarouselThreeD"; import Swiper from "@/components/Swiper"; import * as Constants from "@/constants"; import useMobile from "@/hooks/useMobile"; @@ -180,36 +181,58 @@ export default function ResumeProject() { }; return ( -
-
-

项目经历

+
+
+
- {isMobile ? ( - - {renderSlides()} - - ) : ( - + className={classNames( + "relative z-10 flex h-full flex-col items-center justify-center", + { + "!w-full": isMobile, + } )} - {/* +
+

项目经历

+
+ +
+ {isMobile ? ( + + {renderSlides()} + + ) : ( + + )} + {/* {renderSlides()} */} +