基于svg mask实现的tab切换效果

本文介绍了如何使用SVG的mask属性实现一种tab切换的动画效果。通过介绍SVG mask的工作原理,展示了如何创建和移动遮罩来控制显示内容。文章提供源码,并提及CSS mask的兼容性问题,建议使用SVG mask作为替代方案。最后,作者分享了个人学习心得,计划研究更多前端技术以提升自身能力。

效果

tab切换效果

第一次看到这个效果是在阿里妈妈MUX的博客:UI动效—细微交互,极致体验,里面搜集的一些微交互都让人眼前一亮,恰好自己最近要做一个按钮组,就参考了里面Sergey Valiukh的一个设计。

源码

和上两篇博客一样,相对简单,为了节省你的阅读时间,直接上源码:

上两篇博客:
D3.js SVG绘图实践:趋势缩略图
D3.js SVG绘图实践:波浪动画

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>transition-tabs</title>
    <link rel="stylesheet" href="transition-tabs.css">
  </head>
  <body>
    <div class="transition-tabs">
      <svg width="100%" height="100%">
        <defs>
          <mask id="mask">
            <
<?xml version="1.0" encoding="UTF-8"?> <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <title>icon/参数管理</title> <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="icon1.0" transform="translate(-63, -283)"> <g id="icon/参数管理" transform="translate(63, 283)"> <rect id="矩形" x="0" y="0" width="16" height="16"></rect> <g id="编组" transform="translate(1, 2.1884)"> <path d="M12.8050497,0 L1.19522364,0 C1.19522364,0 1.19522364,0 1.19522364,0 C0.535119849,1.21259098e-16 -8.08393989e-17,0.535119849 0,1.19522364 L-2.22044605e-16,10.4680485 C-1.41214451e-16,11.1280768 0.535058651,11.6631355 1.19508695,11.6631355 C1.19508695,11.6631355 1.19508695,11.6631355 1.19508695,11.6631355 L12.8049131,11.6631355 C12.8049131,11.6631355 12.8049131,11.6631355 12.8049131,11.6631355 C13.4649413,11.6631355 14,11.1280768 14,10.4680485 L14,1.19508695 C14,0.535058651 13.4649413,-1.21245231e-16 12.8049131,0 C12.8049131,0 12.8049131,0 12.8049131,0 L12.8050497,0 Z" id="形状" fill="#272E3B"></path> <path d="M7.00013669,3.0043221 C7.34202725,3.0043221 7.62380813,3.26168223 7.6623182,3.59324135 L7.66680336,3.67098877 L7.66680336,11.6710343 C7.66680336,12.0392242 7.36832652,12.337701 7.00013669,12.337701 C6.65824613,12.337701 6.37646525,12.0803409 6.33795518,11.7487817 L6.33347002,11.6710343 L6.33347002,3.67098877 C6.33347002,3.30279894 6.63194686,3.0043221 7.00013669,3.0043221 Z" id="路径备份-15" fill="#F9F9F9" transform="translate(7.0001, 7.671) rotate(90) translate(-7.0001, -7.671)"></path> <path d="M7.00013669,-0.652262295 C7.34202725,-0.652262295 7.62380813,-0.394902169 7.6623182,-0.0633430457 L7.66680336,0.0144043712 L7.66680336,8.01444993 C7.66680336,8.38263977 7.36832652,8.6811166 7.00013669,8.6811166 C6.65824613,8.6811166 6.37646525,8.42375647 6.33795518,8.09219735 L6.33347002,8.01444993 L6.33347002,0.0144043712 C6.33347002,-0.353785462 6.63194686,-0.652262295 7.00013669,-0.652262295 Z" id="路径备份-16" fill="#F9F9F9" transform="translate(7.0001, 4.0144) rotate(90) translate(-7.0001, -4.0144)"></path> <path d="M4.9214126,2.75884124 C5.26330316,2.75884124 5.54508404,3.01620136 5.58359411,3.34776049 L5.58807927,3.4255079 L5.58807927,4.6033464 C5.58807927,4.97153624 5.28960243,5.27001307 4.9214126,5.27001307 C4.57952204,5.27001307 4.29774116,5.01265294 4.25923109,4.68109382 L4.25474593,4.6033464 L4.25474593,3.4255079 C4.25474593,3.05731807 4.55322277,2.75884124 4.9214126,2.75884124 Z" id="路径备份-17" fill="#F9F9F9" transform="translate(4.9214, 4.0144) rotate(-180) translate(-4.9214, -4.0144)"></path> <path d="M9.12533562,6.41542563 C9.46722618,6.41542563 9.74900706,6.67278576 9.78751714,7.00434488 L9.79200229,7.0820923 L9.79200229,8.2599308 C9.79200229,8.62812063 9.49352546,8.92659747 9.12533562,8.92659747 C8.78344507,8.92659747 8.50166418,8.66923734 8.46315411,8.33767822 L8.45866896,8.2599308 L8.45866896,7.0820923 C8.45866896,6.71390247 8.75714579,6.41542563 9.12533562,6.41542563 Z" id="路径备份-18" fill="#F9F9F9" transform="translate(9.1253, 7.671) rotate(-180) translate(-9.1253, -7.671)"></path> </g> </g> </g> </g> </svg> 从第二个path开始到最后 镂空 直接给我返回svg就行
09-06
/* eslint-disable react/jsx-no-useless-fragment */ /** * H5_热门赛事 */ import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; import { Carousel } from 'antd'; import { JumboTabs } from 'antd-mobile'; import './index.less'; import { getNls, locale } from '@src/utils/i18n'; import { aLinkProps, filterUri } from '@src/utils/common'; import { RightOutlined } from '@ant-design/icons'; import listDefaultImg from '@src/assets/common/listDefaultImg.svg'; import defaultCoverSvg from '@src/assets/common/defaultCover.svg'; import { redirectNewCon } from '@src/h5/utils/appCommon'; import constants from '@src/configs/constants'; import { getThumbnail } from '@src/utils/media'; const dict = getNls('routes_h5_contest_home'); const onError = (e) => { let img = e.target; img.src = defaultCoverSvg; img.onError = null; img.setAttribute('style', 'object-fit:cover'); }; function RacesSlots(props) { const { contestSoltList, questionSoltList } = props; const [onKey, setOnKey] = useState(''); if (contestSoltList?.length === 0 && questionSoltList?.length === 0) return null; const imgRedirect = (e, item) => { let url = ''; const { columnType, contentId, customLink } = item; if (customLink) { url = customLink; } else { if (['developer', 'DEVELOPER'].includes(item?.ext?.origin)) { const TYPE = { racesQuestion: 'question', races: 'competition', }; url = constants.racesDetail.replace(':type', TYPE[columnType]).replace(':contentId', contentId); } else { if (columnType) { const ROUTE = { racesQuestion: 'competitionsDetail', races: 'contestsDetail', }; const type = ROUTE[columnType]; url = constants[type].replace(':id', contentId); } } } if (!url) return; e.stopPropagation(); redirectNewCon(url); }; const courseSlotsList = (list) => { return list ?.map((item1, i) => { if (i % 2 === 0 && i < list.length - 1) { const item2 = list[i + 1]; return ( <div className="topSummitsList" key={`openSource-course${i}`}> <a key={i} className="topSummitsItem" {...{ ...aLinkProps }} {...{ onClick: (e) => { imgRedirect(e, item1); }, }} > <img src={getThumbnail(item1?.appCover, { width: Math.ceil((window.innerWidth - 48) / 2), height: Math.ceil((window.innerWidth - 48) / 4), //比例为2:1 }) || defaultCoverSvg} {...{ onError }} /> <div className="mask" /> <div className="lineLimit2 topSummitsItemTitle h5ap-font-14i">{item1?.customTitle?.[locale]}</div> <div className="lineLimit1 itemSubTitle h5ap-font-12i">{item1?.description?.[locale]}</div> </a> <a key={i + 1} className="topSummitsItem" {...{ ...aLinkProps }} {...{ onClick: (e) => { imgRedirect(e, item2); }, }} > <img src={getThumbnail(item2?.appCover, { width: Math.ceil((window.innerWidth - 48) / 2), height: Math.ceil((window.innerWidth - 48) / 4), //比例为2:1 }) || defaultCoverSvg} {...{ onError }} /> <div className="mask" /> <div className="lineLimit2 topSummitsItemTitle h5ap-font-14i">{item2?.customTitle?.[locale]}</div> <div className="lineLimit1 itemSubTitle h5ap-font-12i">{item2?.description?.[locale]}</div> </a> </div> ); } if (list.length === 1) { let aProps = { className: 'topSummitsItem', onClick: (e) => { imgRedirect(e, item1); }, ...aLinkProps, }; return ( <div className="topSummitsList" key={`openSource-course${i}`}> <a key={i} {...aProps}> <img src={getThumbnail(item1?.appCover, { width: Math.ceil((window.innerWidth - 48) / 2), height: Math.ceil((window.innerWidth - 48) / 4), //比例为2:1 }) || defaultCoverSvg} {...{ onError }} /> <div className="mask" /> </a> </div> ); } return null; }) .filter(Boolean); }; const link = constants.moreEvents; const moreDom = () => { const jumpMore = () => { redirectNewCon(filterUri(link)); }; return ( <div className="moreDomCss" onClick={jumpMore}> <span className="moreText">{dict.more}</span> <RightOutlined /> </div> ); }; const jumboTabsOnChange = (val) => { setOnKey(val); }; return ( <div className="csw-h5-contest-racesSlots"> <div className="csw-h5-contest-racesSlots-content"> <JumboTabs defaultActiveKey="question" onChange={jumboTabsOnChange}> {questionSoltList?.length > 0 ? ( <JumboTabs.Tab key="question" title={ <span className={`${locale === 'zh' ? 'h5ap-font-16' : 'h5ap-font-14'}`}> {dict.hotCompetitionQuestions} </span> } > <Carousel className="csw-h5-contest-topSummitsCarousel" {...{ autoplay: false, infinite: false }}> {courseSlotsList(questionSoltList)} </Carousel> </JumboTabs.Tab> ) : null} {contestSoltList?.length > 0 ? ( <> <JumboTabs.Tab key="constest" title={ <span className={`${locale === 'zh' ? 'h5ap-font-16' : 'h5ap-font-14'}`}>{dict.popularEvents}</span> } > <Carousel className="csw-h5-contest-topSummitsCarousel" {...{ autoplay: false, infinite: false }}> {courseSlotsList(contestSoltList)} </Carousel> </JumboTabs.Tab> </> ) : null} </JumboTabs> {onKey === 'constest' && moreDom()} </div> </div> ); } RacesSlots.propTypes = { contestSoltList: PropTypes.array, questionSoltList: PropTypes.array, }; export default RacesSlots; 这是完整代码,请你使用CapsuleTabs来让该部分支持左右滑动,无需更改其他部分代码
最新发布
11-21
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值