前端可视化学习:如何生成简单动画让图形动起来LearnFE
在可视化展现中,动画它是强化数据表达,吸引用户的重要技术手段。
在具体实现动画之前,我们先来了解一下动画的三种形式,分别是固定帧动画、增量动画和时序动画。
graphLRA[动画的三种形式]-->B[固定帧动画]A-->D[增量动画]A-->E[时序动画]B-->F[使用已生成的静态图像,将图像依次播放]D-->C[动态绘制图像]E-->C固定帧动画的实现,是使用已生成的静态图像,然后将这些图像依次播放,而后面两种,增量动画和时序动画,都是需要动态绘制图像。可想而知,后面这两种动画形式会更灵活一些。
接下来,我们就来了解如何在HTML/CSS和Shader中实现动画效果。
首先,我们来了解如何在HTML/CSS中实现动画。
先来看固定帧动画的一个例子,这个代码实现的是一个飞动的小鸟。
e.g.动态的小鸟
/*固定帧动画*/.fixed-frame{position:absolute;left:100px;top:100px;width:86px;height:60px;zoom:0.5;background-repeat:no-repeat;background-image:url("@/assets/bird.png");background-position:-178px-2px;animation:flappy.5sstep-endinfinite;}@keyframesflappy{0%{background-position:-178px-2px;}33%{background-position:-90px-2px;}66%{background-position:-2px-2px;}}很显然,在实现这个固定帧动画之前,我们需要预先准备好静态图片,这个例子中我们使用的是雪碧图,也叫CSS精灵,是将小图合并在一起形成的图片,在这里我们设置background-image来指定背景图,然后通过animation动态修改background-position来逐帧切换,最终形成一个动态的效果。当然如果我们使用的是多张图片,直接切换background-image也是可以的。
其中step-end会使keyframes动画到了定义的关键帧处直接突变,没有变化的过程。
通过这个例子我们能发现,固定帧动画实现起来非常简单,比较适合的场景是提供现成图片的动画帧图像,如果要去动态绘制图像,就不太合适。如果要生成动态绘制的图像,也就是非固定帧动画,通常会使用另外两种方式。
先来看增量动画,其实从名称上看,我们就能有一个大致的概念,增量嘛,就是增加数量,所以增量动画就是在动画的每一帧给属性一个增量。
下面是一个简单的旋转方块的动画例子,是一个旋转的蓝色方块。
/*增量动画*/.increase-frame{position:absolute;left:100px;top:100px;width:100px;height:100px;background-color:blue;transform-origin:50%50%;}letrotation=0;requestAnimationFrame(functionupdate(){increaseRef.value.style.transform=`rotate(${rotation++}deg)`;requestAnimationFrame(update);});以上动画实现的关键逻辑就在于修改rotation的值,在每次绘制的时候将它加1。
这种绘制方式实现起来也比较简单,但是它不太容易去控制动画的细节,比如动画周期、变化率、轨迹等等;而且它定义的是状态变化,也就是根据上一刻的状态来计算得到下一刻的状态,这种方式在Shader中实现起来并不太方便,需要像上期视频所提到的那样,去使用后期通道来进行处理,很显然,这样做会比较繁琐。
关于如何去实现时序动画,我们也直接来看个例子。
e.g.旋转的蓝色方块
根据这个例子,我们可以将时序动画的实现总结为三个步骤:
第三步,通过进度来更新动画元素的属性。
时序动画的优点是,可以更直观、精确地控制动画的周期(也是速度)等参数;它的缺点就是写法相对比较复杂,但是因为它的优点、可以更好控制动画的效果,所以在动画实现中最为常用。
既然时序动画是最常用的动画实现形式,那么我们可以把它的三个步骤抽象成标准的动画模型,来方便后续的动画实现。
现在我们就可以使用这个模型,来尝试实现动画效果了。来看下面这个例子。
在这个例子中,我们让每个方块转动的周期是1秒,一共旋转1.5个周期(也就是540度)。
constblocks=document.querySelectorAll('.block');constanimator=newAnimator({duration:1000,iterations:1.5});(asyncfunction(){leti=0;while(true){awaitanimator.animate(blocks[i++%4],({target,timing})=>{target.style.transform=`rotate(${timing.p*360}deg)`;});}}());.container{display:flex;flex-wrap:wrap;justify-content:space-between;width:300px;}.block{width:100px;height:100px;margin:20px;flex-shrink:0;transform-origin:50%50%;&:nth-child(1){background-color:red;}&:nth-child(2){background-color:blue;}&:nth-child(3){background-color:green;}&:nth-child(4){background-color:orange;}}
可以看到,这个效果我们很方便地通过前面定义的Animator实现了。
在前面的例子中,我们看到的动画效果都是匀速运动的,图像是匀速变化的,显然在实际中这是不够满足需求的,既然时序动画可以让我们更容易地控制动画的细节,所以它也可以让我们实现一些不规则的运动。
假设已知元素的起始状态、结束状态和运动周期,如果想要让它进行不规则运动,我们可以使用插值的方式来控制每一帧的展现。
下面我们来看一个动画:这是一个匀速运动的方块,我们用Animator实现,让这个方块从100px处匀速运动到400px。
constblock=document.querySelector('.block');constanimator=newAnimator({duration:3000});document.addEventListener('click',()=>{animator.animate({el:block,start:100,end:400},({target:{el,start,end},timing:{p}})=>{constleft=start*(1-p)+end*p;el.style.left=`${left}px`;});});
这里我们用了一个线性插值方法:left=start*(1-p)+end*p。线性插值可以很方便地实现属性的均匀变化,所以用它来让方块做匀速运动是非常简单的。
如果要让方块进行非匀速运动,比如匀加速运动,我们仍然可以用线性插值的方式,只不过要对参数p做一个函数映射。比如要让方块做初速度为0的匀加速运动,我们可以将p映射为p的平方;如果要让方块做末速度为0的匀减速运动,可以将p映射为p*(2-p)。那为什么是这样映射呢?
这就要提到匀加速和匀减速的物理计算公式了。有些小伙伴很久没接触物理公式,可能会有些遗忘,这里简单回顾一下。
$$a=\frac{2S}{T^2}\\S_t=\frac{1}{2}at^2=S(\frac{t}{T})^2=Sp^2$$
所以在匀加速运动中,我们把p映射为p的平方。
同样的,如果物体在做匀减速运动,那么,它的加速度和在t时刻的位移的计算公式是这样的:
$$a=-\frac{2S}{T^2}\\S_t=\frac{2S}{T}t-S(\frac{t}{T})^2=Sp(2-p)$$
所以在匀减速运动中,我们把p映射为p*(2-p)。
在实际应用中,我们还可以对p应用更多映射,来实现不同的动画效果,为了方便实现更多的效果,我们可以抽象出一个函数来专门处理p的映射,这个函数就叫做缓动函数。
我们可以在Timing类中直接增加一个缓动函数easing,在获取p的时候,直接用this.easing(progress%1)取代progress%1。
现在我们可以来尝试使用下缓动函数。
constanimator2=newAnimator({duration:3000,easing:p=>p**2});document.addEventListener('click',()=>{animator2.animate({el:block,start:100,end:400},({target:{el,start,end},timing:{p}})=>{constleft=start*(1-p)+end*p;el.style.left=`${left}px`;});});
constanimator3=newAnimator({duration:3000,easing:BesizerEasing(0.5,-1.5,0.5,2.5)});document.addEventListener('click',()=>{animator3.animate({el:block,start:100,end:400},({target:{el,start,end},timing:{p}})=>{constleft=start*(1-p)+end*p;el.style.left=`${left}px`;});});
看到这里,关于如何去实现动画,相信大家都有一定的思路了。那么现在我们也可以尝试在Shader中去实现动画效果。
首先我们还是先来看固定帧动画的实现。
直接来看具体的例子,还是之前那个飞动的小鸟的例子。
//片元着色器varyingvec2vUv;uniformsampler2DtMap;uniformfloatfWidth;uniformvec2vFrames[3];//3个二维向量,二维向量表示每一帧动画的图片起始x和结束x坐标uniformintframeIndex;voidmain(){vec2uv=vUv;for(inti=0;i<3;i++){//纹理坐标ux.x的取值范围//第0帧:[2/272,88/272]约等于[0.007,0.323]//第1帧:[90/272,176/272]约等于[0.330,0.647]//第2帧:[178/272,264/272]约等于[0.654,0.970]uv.x=mix(vFrames[i].x,vFrames[i].y,vUv.x)/fWidth;//vUv到uv的映射if(float(i)==mod(float(frameIndex),3.0))break;//frameIndex除3的余数:0-循环一次;1-循环两次;2-循环三次。(渲染第几帧)}vec4color=texture2D(tMap,uv);//按照uv坐标取色值gl_FragColor=color;}我们在片元着色器中获取纹理,通过纹理坐标读取图像上的像素信息。
vFrames是一个重要的参数,包含3个二维向量,每一个二维向量表示一帧图片的起始x和结束x坐标。
for循环是main函数中的关键部分,在循环内部,我们用二维向量中的两个坐标,来计算插值,最后除以图片的总宽度,得到一个vUv到uv坐标映射。
在对纹理进行采样时,我们就用这个uv的坐标值去进行颜色提取。
然后看JavaScript部分的代码:
(asyncfunction(){renderer.uniforms.tMap=awaitrenderer.loadTexture(birdpng);renderer.uniforms.vFrames=[2,88,90,176,178,264];renderer.uniforms.fWidth=272;renderer.uniforms.frameIndex=0;setInterval(()=>{renderer.uniforms.frameIndex++;},200);//顶点坐标(WebGL画布绘制范围)constx=43/glRef.value.width;//每帧的宽度(86/2)consty=30/glRef.value.height;//每帧的高度(60/2)renderer.setMeshData([{positions:[[-x,-y],[-x,y],[x,y],[x,-y]],attributes:{uv:[[0,0],[0,1],[1,1],[1,0]]},cells:[[0,1,2],[2,0,3]]}]);renderer.render();}());我们按照每帧图片的宽高比例设置了顶点坐标的范围,vFrames数组存储的是每一帧图像对应的x坐标范围,动画切换的关键代码就是setInterval中的frameIndex++。
可以看到在Shader中实现固定帧动画也是比较简单的。
对于非固定帧动画,因为时序动画是最常用的实现形式,所以我们直接看时序动画。
大家都知道,在WebGL中有两类着色器,那么对动画的实现应该写在哪类着色器中呢?答案是,两个都可以。
我们先来看顶点着色器的例子。
在Shader中会绘制出一个红色的正方形,然后三维的齐次矩阵会让这个红色方块旋转起来。我们可以直接通过下面这段JavaScript去动态更新旋转的角度rotation,就能看到动画效果了:
//...renderer.uniforms.rotation=0.0;requestAnimationFrame(functionupdate(){renderer.uniforms.rotation+=0.05;requestAnimationFrame(update);});//...我们也可以使用前面定义的Animator对象去更精确地控制图形的旋转效果。
//...renderer.uniforms.rotation=0.0;constanimator=newAnimator({duration:2000,iterations:Infinity});animator.animate(renderer,({target,timing})=>{target.uniforms.rotation=timing.p*2*Math.PI;});//...可以看到,这里更新uniform属性和前面更新HTML元素的属性,这两种操作从代码上看很相似。
接着我们来看片元着色器的例子。
varyingvec2vUv;uniformvec4color;uniformfloatrotation;voidmain(){vec2st=2.0*(vUv-vec2(0.5));floatc=cos(rotation);floats=sin(rotation);mat3transformMatrix=mat3(c,s,0,-s,c,0,0,0,1);vec3pos=transformMatrix*vec3(st,1.0);//坐标系旋转floatd1=1.0-smoothstep(0.5,0.505,abs(pos.x));//abs(x)<0.5d1=1floatd2=1.0-smoothstep(0.5,0.505,abs(pos.y));//abs(y)<0.5d2=1gl_FragColor=d1*d2*color;}这段代码中,我们通过距离场着色的方式绘制了正方形,同样传递了rotation来控制方块的旋转角度。
我们能很明显的发现,片元着色器和前面顶点着色器的实现,最终实现的效果上,两个方块的旋转方向不一致。顶点着色器中是逆时针旋转,片元着色器中是顺时针旋转,这是因为在顶点着色器中,我们是直接改变了顶点坐标,通过旋转矩阵的处理映射到了新的顶点,而在片元着色器中的坐标变换,相当于是把坐标系做了旋转,最终绘图的图形是相对于新的坐标系去计算距离场的距离,所以最终就呈现了相反的旋转效果。
那么既然两类着色器都能实现动画效果,在实际使用中我们要怎么选择呢?一般来说,动画如果能使用顶点着色器实现,会尽量在顶点着色器中实现。因为在绘制一帧画面的时候,顶点着色器的运算量会大大少于片元着色器,所以使用顶点着色器消耗的性能更少。
但是假如我们需要绘制更复杂的效果,比如运用大量的重复、随机、噪声,那么使用片元着色器更合适。
所以具体的,还是要根据我们最终想要达到的效果、去选择合适的实现方式。
和HTML/CSS中的例子一样,如果我们想要在Shader中实现非匀速运动,也可以直接使用Animator对象,在JavaScript中使用缓动函数,但是在WebGL中除了这种方式之外,我们也可以选择直接把缓动函数写在Shader中,比如下面这个例子:
//vertexattributevec2a_vertexPosition;uniformvec4uFromTo;uniformfloatuTime;floateasing(infloatp){//returnsmoothstep(0.0,1.0,p);//returnclamp(p*p,0.0,1.0);//匀加速returnclamp(p*(2.0-p),0.0,1.0);//0->1->0//先减速后加速//if(p<1.0)returnclamp(p*(2.0-p),0.0,1.0);//elsereturn1.0;}voidmain(){gl_PointSize=1.0;vec2from=uFromTo.xy;vec2to=uFromTo.zw;floatp=easing(uTime/2.0);vec2translation=mix(from,to,p);mat3transformMatrix=mat3(1,0,0,0,1,0,translation,1);vec3pos=transformMatrix*vec3(a_vertexPosition,1);gl_Position=vec4(pos,1);}可以用smoothstep(0.0,1.0,p)来让方块做平滑变速运动;也可以替换缓动函数,使用比如clamp(p*p,0.0,1.0)或clamp(p*(2.0-p),0.0,1.0)来实现匀加速、匀减速的运动效果。
THE END
1.人工智能为跨学科学习带来机遇《义务教育课程方案(2022年版)》明确提出,“强化课程综合性和实践性,推动育人方式变革,着力发展学生核心素养”“强化学科内知识整合,统筹设计综合课程和跨学科主题学习”,强调在真实情境中培养学生运用跨学科知识解决问题的能力。跨学科学习是当前基础教育改革和课程建设的重点,然而当前仍存在学科之间的简单拼接、资源整合http://www.jyb.cn/rmtzgjsb/202412/t20241210_2111281739.html
2.ReinforcementLearning原理与代码实例讲解强化学习是一种基于交互学习的机器学习方法,它允许智能体通过与环境的交互来学习最优的行为策略。智能体在环境中采取行动,并根据环境的反馈(奖励或惩罚)调整其策略,以最大化累积的奖励。 2. 核心概念与联系 核心概念: 智能体 (Agent):学习和决策的实体,例如机器人、游戏玩家或算法。 https://blog.csdn.net/2301_76268839/article/details/144287840
3.[转]新课程视域下项目式学习行动路径的建构为此,研究者从“项目”这一词汇在教育学和社会实践两个范畴的发展阶段辨析开始,就中国式现代化对教学改革的需要、项目式学习过程与结果的双重表达、回归教材体系等问题进行深入探讨,结合教育改革的现代化发展趋势,基于教师与学生的双重视角,提出开展项目执行架构的教育化改造,建构基于教材单元的项目式学习行动路径,倡导规https://yun.zjer.cn/space/index.php?r=space/person/blog/view&sid=358362&id=39543274
4.GitHubauto基于Autojs的增量式开发安卓脚本应用软件——强国助手(以学习强国应用实战开发). Contribute to auto-js/LearnChinaHelper development by creating an account on GitHub.https://github.com/auto-js/LearnChinaHelper
5.增量学习机器之心增量学习早在1986年就已经存在,但是直到2001年,Kuncheva对增量学习的定义进行了规范,并被普遍接受。在接下来的几年,增量学习被广泛的应用到不同的领域,包括图像,视频跟踪等。在2009年和2011年,两种增量学习的改进算法:Learn++.NSE和Learn++.NC被提出,进一步提高了增量学习算法的应用范围。 https://www.jiqizhixin.com/graph/technologies/09134d6a-96cc-409b-86ef-18af25abf095
6.基于Learn++的软测量建模新方法软测量Learn++智能模型ELM增量学习 分类号: TP206(自动化技术及设备) 资助基金: 国家自然科学基金(60674063) 在线出版日期: 2015-07-29(万方平台首次上网日期,不代表论文的发表时间) 页数: 4(30-33) 参考文献 (10) 仅看全文 排序: 发表时间 被引频次 https://d.wanfangdata.com.cn/periodical/dbdxxb200901008
7.校本研修生成式人工智能在教学中的应用生成式人工智能技术能够创造文本、图片、声音、视频和代码等多种类型的内容,正在重塑当下及未来教学与学习方式,已经成为推动教育创新的重要力量。 我校一直致力于推动教师数字素养提升,下一步,学校将继续加强该系列培训,鼓励教师积极探索和应用新技术,将前沿技术融入到https://mp.weixin.qq.com/s?__biz=MzIyMDMyNzk1Mg==&mid=2247618813&idx=1&sn=be21ce2893c28362d1cf23c5cfa60cca&chksm=962e757ab6b2ee8afeefcfda147df33e5aaa8defe2299d2c4272150f9d7acd3f4abb770fe085&scene=27
8.机器学习K聚类深度自动编码器kmeans聚类算法原理与步骤也可以看看MiniBatchKMeans替代性在线实施,使用微型批次对中心位置进行增量更新。对于大规模学习(例如n_samples> 10k),MiniBatchKMeans可能比默认的批处理实现要快得多。 sklearn.cluster.k_means sklearn.cluster.k_means(X, n_clusters, *, sample_weight=None, init='k-means++', precompute_distances='deprechttps://blog.51cto.com/u_16099252/10888660
9.基于增量学习的深度人脸伪造检测本文主要有以下贡献:1)在已有的增量学 习框架 DER[29]上进行改进,使其适应人脸伪造 检测任务;2)设计 3 种分类学习系统,加强分类 器的判别能力;3)在实验定义的 FF++扩充集(包含 4 种伪造人脸及相应的真实人脸)和 Forg- eryNet 扩充集(包含 15 种伪造人脸及相应的真 实人脸)上进行测试,结果显示本文方法http://www.jfdc.cnic.cn/EN/article/downloadArticleFile.do?attachType=PDF&id=353
10.动态梯度调制平衡视听学习,BalancedAudioCVPR 2022 | ST++: 半监督语义分割中更优的自训练范式 极市平台 0+阅读 · 2022年3月11日 【ICLR2022】基于任务相关性的元学习泛化边界 专知 2+阅读Learning to Learn and Predict: A Meta-Learning Approach for Multi-Label Classification Arxiv 17+阅读 · 2019年9月10日 Continual Lifelong Learning https://zhuanzhi.ai/vip/f667c91c00ea91a11ec74da7c1e1bbdb
11.sklearn官网,ScikitSklearn是什么? sklearn,全称Scikit-learn(以前称为scikits.learn)是针对Python编程语言的免费软件机器学习库 。它具有各种分类,回归和聚类算法,包括支持向量机,随机森林,梯度提升,k均值和DBSCAN,并且旨在与Python数值科学库NumPy和SciPy联合使用。其具体功能如下图所示: https://feizhuke.com/sites/sklearn.html
12.基于样本密度和分类误差率的增量学习矢量量化算法研究2) 增 量法[13], 即用部分原始数据构建基本分类器模型, 训练后, 只保存训练好的模型, 而将原始样本抛弃, 并不断学习其他原始数据, 进而得到原型集, 目前 已有算法如基于增量支持向量机 (Support vector machine, SVM) 的原型算法[14],增量聚类原型算 法[15?16],增量学习矢量量化 (Incremental learn- http://www.aas.net.cn/CN/article/downloadArticleFile.do?attachType=PDF&id=18693
13.ControllerwithC++)91人已学习 爱给网提供海量的虚幻资源素材免费下载, 本次作品为mp4 格式的117. 用C获取播放器控制器++(117. Get the Player Controller with C++), 本站编号36656549, 该虚幻素材大小为40m, 时长为09分 04秒, 支持4K播放, 不同倍速播放 作者为JacPete, 更多精彩虚幻素材,尽在爱给网。https://www.aigei.com/item/udemy_learn_t_116.html
14.ConcreteSubspaceLearningbasedInterferenceEliminationfor(a) Meta-learn the Concrete mask (b) Comparing AdaMerging with and without the Concrete mask 图3: AdaMerging 和 Concrete AdaMerging 之间的性能比较。 这里我们展示了将 AdaMerging 和 Concrete AdaMerging 应用到 CLIP-ViT-B/32 的整个过程,y 轴由这两个子图共享:(a) 显示了合并模型在元学习阶段https://yiyibooks.cn/__trs__/arxiv/2312.06173v1/index.html
15.贝叶斯定理(精选十篇)与此方法相比,Polikar[3]面向监督学习任务,定义增量学习算法应当满足从新数据中学习新的知识,不需要访问当前分类器学习过的数据,仅保存当前所获得的知识,可以适应具有新的类别标记的样本。基于这种思想,Polikar等人提出了一种针对有监督任务的增量学习算法Learn++,增量地训练多层感知机(MLP),并将之运用于分类任务中。对https://www.360wenmi.com/f/cnkey81bxk5w.html
16.机器学习学术速递[6.21]腾讯云开发者社区Graph相关(图学习|图神经网络|图优化等)(9篇) 【1】 Self-supervised Incremental Deep Graph Learning for Ethereum Phishing Scam Detection 标题:自监督增量式深度图学习在以太网络钓鱼检测中的应用 作者:Shucheng Li,Fengyuan Xu,Runchuan Wang,Sheng Zhong 机构:National Key Lab for Novel Software Technologyhttps://cloud.tencent.com/developer/article/1841500
17.分支和循环C#教程简介此示例新引入了另外一个运算符。counter变量后面的++是增量运算符。 它将counter值加 1,并将计算后的值存储在counter变量中。 可以在自己的开发环境中继续学习数组和集合教程。 若要详细了解这些概念,请参阅下列文章: 选择语句 迭代语句 其他资源 培训 https://learn.microsoft.com/zh-cn/dotnet/csharp/tour-of-csharp/tutorials/branches-and-loops-local
18.机器学习各语言领域工具库中文版汇总DLib– DLib有C ++和Python脸部识别和物体检测接口。 EBLearn– Eblearn是一个面向对象的C ++库,实现了各种机器学习模型。 VIGRA– VIGRA是一个跨平台的机器视觉和机器学习库,可以处理任意维度的数据,有Python接口。 通用机器学习 MLPack– 可拓展的C ++机器学习库。 https://www.jianshu.com/p/b81680a52cd7
19.C++博客春暖花开随笔分类The first 78 were computer books (number 79 was Learn Bengali in 30 days). I replaced "days" with "hours" and got remarkably similar results也愿与c++博客的各位朋友分享我的学习心得。 步入主题。 这一章开篇介绍了windows函数的几种返回值:VOID,BOOL,HANDLE,PVOID,LONG/DWORD。让我们明白,仅仅http://www.cppblog.com/SpringSnow/category/8492.html/rss
20.c语言学习笔记期末不挂科!指针的运算 指针的移动:对指针实施+、-、*、/,指针值变化的最小单位是sizeof(T)T是类型名 p+n引起物理地址的增量n×sizeof(T) ++和–的指针算术运算 1.前缀式++p或–p ++p使指针p先自增指向其后的一个元素,–p使指针p自减指向其前面的一个元素,然后再进行其他操作。 2.后缀式p++和p– 此时使用的https://easylearn.baidu.com/edu-page/tiangong/exercisedetail?id=f02b0e8b8c9951e79b89680203d8ce2f01666504&fr=search
21.gzoftju/gzoft202308011423431:精选了千余项目,包括机器学习bangoc123/learn-machine-learning-in-two-months 在2 个月内学习好机器学习所需的知识。 ukas/ml-class 专为工程师设计的机器学习课程和教学项目 Mohitkr95/Best-Data-Science-Resources 该存储库包含最好的数据科学免费精选资源,可为您提供所有行业驱动的技能和面试准备工具包。 academic/awesome-datascience 很https://openi.pcl.ac.cn/gzoftju/gzoft202308011423431
22.课程网站的学习资料及手机学习通等在本单元学习中的作用体现?C Bored?Lonely?Out of condition? Why not try the SPORT CEN7IER? TENNIS Indoor and outdoor courts.Coaching from beginners to advanced,everyday not evenings.Children only—Sat.mornings. SKⅡNG Dry slhttps://www.shuashuati.com/ti/73113cb69da3459987acc41fdf4b8b58.html