关于svg动画:Logo小变动心境大不同SVG矢量动画格式网站Logo图片制作与实践教程Python3

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_207 曾几何时,SVG(Scalable Vector Graphics)矢量动画图被坊间称之为一种被浏览器咒骂的技术,只因为蹩脚的硬件反对(IE),和没完没了的兼容性调优(Safari)。然而在2022年的明天,所有都不一样了,正所谓三十年河东,三十年河西,微软所研发的采纳Chromium内核作为IE替代者的Edge浏览器曾经无望超过Safari成为第二大桌面浏览器,而已经因为不反对关键帧动画被人诟病的Safari也对立了规范,市面上除了老帮菜IE,简直所有平台(包含挪动端)都曾经对SVG足够敌对,这让咱们能够放心大胆的在网站上利用SVG矢量动画图。 目前国内外相对来说技术后行的平台都曾经采纳SVG矢量格局,比方线上媒体界的巨擘Netflix、社交平台Twitter和国内的B站: 总的来说,采纳SVG格局图像的益处还是比拟多的,因为 SVG 图像是矢量图像,能够有限缩放,而且在图像品质降落方面没有任何问题。为什么会这样呢?因为 SVG 图像是应用 XML 标记构建的,浏览器通过绘制每个点和线来打印它们。这确保 SVG 图像能够适应不同的屏幕大小和分辨率,传统图像格式(如 PNG、GIF 或 JPG)是通过预约义的像素来填充色调空间,这就导致屏幕素质会对图像的成像品质产生影响。 举个例子,传统像素图片就像是食堂里曾经烙好的饼,是多大就是多大,碰上胃口好的,饼不够大,就会产生影响,而SVG是通过XML技术把饼的轮廓和技术参数定义好,由浏览器实时绘制,能够依据胃口的大小自适应饼的大小,达到矢量的目标。同时,因为是在 XML 中定义的,SVG 图像比 JPG 或 PNG 图像更灵便,而且咱们能够应用 CSS 和 JavaScript 与它们进行交互。另外从文件体积角度上讲,SVG并不比PNG更大,反而压缩之后体积更小,最初作为XML的网页格局,间接在浏览器中解析,所以不须要独自的带宽进行申请,节约了网络申请数,百利而无一害。 接下来的传统节目应该是介绍SVG根本语法,而后画几个不痛不痒的简略矢量图,展示一下SVG个性,最初草草了事,那样就太无趣了,本次咱们来点“快餐”,简略粗犷的将PNG的Logo图像间接转化成SVG格局,略过“绘制”的步骤,五分钟内间接让你的网站Logo“芜湖腾飞”。 SVG图像转化与压缩以本站的Logo为例子,首先须要一个PNG矢量图: 留神图像色彩位数最好越小越好,这样转化后的图像不会过大,同时背景最好是通明的,因为通明元素不会被XML文件进行标记,达到节约空间的目标,这里咱们以PNG仿色4位的位图为例子。 转化形式有很多种,能够通过Python3的三方图像库: pip3 install Pillow编写转化脚本test.py: import sys import os import operator from collections import deque import io from optparse import OptionParser from PIL import Image def add_tuple(a, b): return tuple(map(operator.add, a, b)) def sub_tuple(a, b): return tuple(map(operator.sub, a, b)) def neg_tuple(a): return tuple(map(operator.neg, a)) def direction(edge): return sub_tuple(edge[1], edge[0]) def magnitude(a): return int(pow(pow(a[0], 2) + pow(a[1], 2), .5)) def normalize(a): mag = magnitude(a) assert mag > 0, "Cannot normalize a zero-length vector" return tuple(map(operator.truediv, a, [mag] * len(a))) def svg_header(width, height): return """<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg" version="1.1"> """ % (width, height) def rgba_image_to_svg_pixels(im): s = io.StringIO() s.write(svg_header(*im.size)) width, height = im.size for x in range(width): for y in range(height): here = (x, y) rgba = im.getpixel(here) if not rgba[3]: continue s.write( """ <rect x="%d" y="%d" width="1" height="1" style="fill:rgb%s; fill-opacity:%.3f; stroke:none;" />\n""" % (x, y, rgba[0:3], float(rgba[3]) / 255)) print("Converting pixels: " + str(x * 100 / width) + "%") s.write("""</svg>\n""") return s.getvalue() def joined_edges(assorted_edges, keep_every_point=False): pieces = [] piece = [] directions = deque([ (0, 1), (1, 0), (0, -1), (-1, 0), ]) while assorted_edges: if not piece: piece.append(assorted_edges.pop()) current_direction = normalize(direction(piece[-1])) while current_direction != directions[2]: directions.rotate() for i in range(1, 4): next_end = add_tuple(piece[-1][1], directions[i]) next_edge = (piece[-1][1], next_end) if next_edge in assorted_edges: assorted_edges.remove(next_edge) if i == 2 and not keep_every_point: # same direction piece[-1] = (piece[-1][0], next_edge[1]) else: piece.append(next_edge) if piece[0][0] == piece[-1][1]: if not keep_every_point and normalize(direction( piece[0])) == normalize(direction(piece[-1])): piece[-1] = (piece[-1][0], piece.pop(0)[1]) # same direction pieces.append(piece) piece = [] break else: raise Exception("Failed to find connecting edge") return pieces def rgba_image_to_svg_contiguous(im, keep_every_point=False): # collect contiguous pixel groups adjacent = ((1, 0), (0, 1), (-1, 0), (0, -1)) visited = Image.new("1", im.size, 0) color_pixel_lists = {} width, height = im.size for x in range(width): for y in range(height): here = (x, y) if visited.getpixel(here): continue rgba = im.getpixel((x, y)) if not rgba[3]: continue piece = [] queue = [here] visited.putpixel(here, 1) while queue: here = queue.pop() for offset in adjacent: neighbour = add_tuple(here, offset) if not (0 <= neighbour[0] < width) or not (0 <= neighbour[1] < height): continue if visited.getpixel(neighbour): continue neighbour_rgba = im.getpixel(neighbour) if neighbour_rgba != rgba: continue queue.append(neighbour) visited.putpixel(neighbour, 1) piece.append(here) if not rgba in color_pixel_lists: color_pixel_lists[rgba] = [] color_pixel_lists[rgba].append(piece) print("Converting image: " + str(round(x * 100 / width, 2)) + "%") del adjacent del visited # calculate clockwise edges of pixel groups edges = { (-1, 0): ((0, 0), (0, 1)), (0, 1): ((0, 1), (1, 1)), (1, 0): ((1, 1), (1, 0)), (0, -1): ((1, 0), (0, 0)), } color_edge_lists = {} counter = 0 for rgba, pieces in color_pixel_lists.items(): for piece_pixel_list in pieces: edge_set = set([]) for coord in piece_pixel_list: for offset, (start_offset, end_offset) in edges.items(): neighbour = add_tuple(coord, offset) start = add_tuple(coord, start_offset) end = add_tuple(coord, end_offset) edge = (start, end) if neighbour in piece_pixel_list: continue edge_set.add(edge) if not rgba in color_edge_lists: color_edge_lists[rgba] = [] color_edge_lists[rgba].append(edge_set) counter = counter + 1 print("Calculating edges: " + str(round(counter * 100 / len(color_pixel_lists.items()), 2)) + "%") del color_pixel_lists del edges # join edges of pixel groups color_joined_pieces = {} for color, pieces in color_edge_lists.items(): color_joined_pieces[color] = [] for assorted_edges in pieces: color_joined_pieces[color].append( joined_edges(assorted_edges, keep_every_point)) s = io.StringIO() s.write(svg_header(*im.size)) counter = 0 for color, shapes in color_joined_pieces.items(): for shape in shapes: s.write(""" <path d=" """) for sub_shape in shape: here = sub_shape.pop(0)[0] s.write(""" M %d,%d """ % here) for edge in sub_shape: here = edge[0] s.write(""" L %d,%d """ % here) s.write(""" Z """) s.write( """ " style="fill:rgb%s; fill-opacity:%.3f; stroke:none;" />\n""" % (color[0:3], float(color[3]) / 255)) counter = counter + 1 print("Joining edges: " + str(round(counter * 100 / len(color_joined_pieces.items()), 2)) + "%") s.write("""</svg>\n""") return s.getvalue() def png_to_svg(filename, contiguous=None, keep_every_point=None): try: im = Image.open(filename) except IOError as e: sys.stderr.write('%s: Could not open as image file\n' % filename) sys.exit(1) im_rgba = im.convert('RGBA') if contiguous: return rgba_image_to_svg_contiguous(im_rgba, keep_every_point) else: return rgba_image_to_svg_pixels(im_rgba) if __name__ == "__main__": parser = OptionParser() parser.add_option( "-p", "--pixels", action="store_false", dest="contiguous", help= "Generate a separate shape for each pixel; do not group pixels into contiguous areas of the same colour", default=True) parser.add_option( "-1", "--one", action="store_true", dest="keep_every_point", help= "1-pixel-width edges on contiguous shapes; default is to remove intermediate points on straight line edges. ", default=None) (options, args) = parser.parse_args() if (len(sys.argv)) < 2: for file in os.listdir("."): if file.endswith(".png"): print("Converting " + file) f = open(file.replace(".png", ".svg"), 'w') f.write( png_to_svg(file, contiguous=options.contiguous, keep_every_point=options.keep_every_point)) else: for file in sys.argv: if file.endswith(".png"): print("Converting " + file) f = open(file.replace(".png", ".svg"), 'w') f.write( png_to_svg(file, contiguous=options.contiguous, keep_every_point=options.keep_every_point))运行脚本文件: ...

February 23, 2022 · 9 min · jiezi

关于svg动画:公众号SVG序列帧动画

最近在水果公众号上看到一段菊花绽开的动画,丝般光滑的感触,吹弹可破的花瓣,让我忍不住看了下到底,将所得整顿了下,作为小技巧分享给大家。 具体看下图,视频转换成GIF品质降落,有趣味能够去水果公号看。 水果绽开动画 瞄了下源代码,竟然是用SVG动画拼成的序列帧。序列帧是什么?水果前端为什么好好的GIF图不必,要用序列帧?起因听我缓缓道来。 什么是序列帧? 序列帧就是一系列静止图像,将这些图像依照肯定的频率播放,就造成了间断的动画,个别认为到达到每秒24帧的速率,人们才会看到平滑动画,大家平时看到的视频和动图实质都是由序列帧组成的。 电影画面中序列帧 翻页动画 应用序列帧有什么益处? 能够保障图片品质 因为公众号后盾会压缩上传的GIF图片,而且GIF图像自身对透明度反对不佳,对图像品质要求较高的场景就不适合了。而应用序列帧,咱们能够应用无损的PNG图片,保障动画高清显示。动画更平滑 绝对于GIF动画,程序控制的动画用户体验更加平滑晦涩。怎么应用序列帧? 在平时的web开发中,咱们能够很容易地用js或者css实现相似的帧动画,因为公众号图文环境的限度,咱们只能用SVG来实现这个成果,水果公号里用animate标签奇妙的解决这个问题。 通过把每一帧的图片独自放在一层里,并使每一层重叠在一起。应用透明度动画来管制每个图片帧的呈现工夫,产生间断播放的动画成果。 绽开动画的序列帧叠加演示 <div style="height:0;"> <svg opacity="0" viewBox="0 0 828 828" style="width:100%;background-image:url('img/02.png');background-size:100% auto;background-repeat:none;transform:rotateZ(0deg);"> <animate attributename="opacity" begin="1.3125s" dur="6.25s" values="1; 1; 0; 0;" keytimes="0; 0.010; 0.012; 1" fill="freeze"></animate> </svg></div><div style="height:0;"> <svg opacity="0" viewBox="0 0 828 828" style="width:100%;background-image:url('img/01.png');background-size:100% auto;background-repeat:none;transform:rotateZ(0deg);"> <animate attributename="opacity" begin="1.25s" dur="6.25s" values="1; 1; 0; 0;" keytimes="0; 0.010; 0.012; 1" fill="freeze"></animate> </svg></div>原理懂了,可是动画整整有70多帧,复制黏贴显然不适宜我这样(lan)机智的前端,于是拿出了看家本领javascript大法。 const generateFrames = () => { const container = document.querySelector('#container') const totalFrames = 72 let html = '' for(let i = totalFrames; i > 0; i--) { const inter = 0.0625 const height = i == 1 ? 'auto' : '0px' const opacity = i == 1 ? 1 : 0; const begin = (1.5 + inter * i) + 's' const dom = ` <div token interpolation" >${height};"> <svg opacity="${opacity}" viewBox="0 0 828 828" token interpolation" >${i}.png');background-size:100% auto;background-repeat:none;transform:rotateZ(0deg);"> <animate attributename="opacity" begin="${begin}" dur="6.25s" values="1; 1; 0; 0;" keytimes="0; 0.010; 0.012; 1" fill="freeze"></animate> </svg> </div> ` html += dom } container.innerHTML = html}generateFrames()将生成的代码,在开发者工具中复制黏贴。 ...

July 24, 2020 · 1 min · jiezi

对SVG动画进行惰性异步光栅化处理

翻译:疯狂的技术宅原文:http://jakearchibald.com/2017…本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜的前端技术文章渲染SVG图像可能会非常慢在转换 SVG 图像时,浏览器会试着在每一帧上进行渲染,以便使图像尽可能的清晰。 不幸的是,SVG渲染可能会很慢,特别是对于较大的图像。这里是一个DEMO,打开后点击“Scale SVG”查看效果。图:使用 Devtools 查看SVG动画的时间线这是一个非常复杂的SVG,在某些帧上消耗的时间是我们帧预算的10倍,所以这个动画看起来非常糟糕。 这是在一款功能强大的MacBook上做的测试。如果是更简单的SVG,就不那么糟了。 这是用Firefox的logo演示的另一个例子,看起来效果还可以。不过新API为我们提供了更多的控制方法:SVG栅格化的惰性处理createImageBitmap(imgElement).then(imageBitmap => { // … }); createImageBitmap可以将许多不同的图像栅格化为位图数据,这些数据可以绘制到canvas元素上。 但是,在Chrome 61+中,启用了chrome://flags/#enable-experimental-canvas-features,它可以为 SVG 图像启用HTML图像元素,并在主线程之外进行异步的栅格化处理,所以不会破坏动画。另外你还可以只渲染SVG的一部分,并以特定大小进行输出:createImageBitmap( imgElement, sourceCropX, sourceCropY, sourceCropWidth, sourceCropHeight, {resizeWidth, resizeHeight} ).then(imageBitmap => …); 这允许我们非常方便的使用画布对SVG进行位图缩放,同时渲染被裁剪后且非常清晰的版本。 一旦清晰版准备就绪,就可以将其包含在动画中。这里是一个DEMO,按“Scale canvas”查看效果。 需要Chrome 61+ 中查看,并启用 chrome://flags/#enable-experimental-canvas-features 。图:Devtools中画布动画的时间线使用这种方法对CPU来说更加友好,动画也很流畅:查看SVG动画与SVG-in-canvas两种效果比较的视频演示:https://youtu.be/-yQBbWlXuqg对于复杂的汽车SVG图像,最后才会出现清晰的图像。 使用Firefox徽标时,清晰版出现得更早,因为渲染时间更短。DEMO的所有代码:https://glitch.com/edit/#!/sv…:1:0平滑光栅化从上面的时间线可以看出,Chrome在将更清晰的纹理传到GPU时仍然会跳过一帧。 这个问题可以通过将工作分块为更小的块来解决,因此GPU上传不会破坏帧预算。OpenSeadragon:可以动态加载图像切片,并创建可缩放图像。 它非常适合从网络中获取位图数据,但有点hack。Zoomable lazy-rendered tiled SVG: 需要Chrome 61+并启用 chrome://flags/#enable-experimental-canvas-features 。是的,边缘有一点粗糙。 就像我前面说的那样,这是一个hack。 不过我真的对此很兴奋,对 SVG 图像更加酷炫的处理技术在逐渐用于web。本文首发微信公众号:jingchengyideng欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

January 30, 2019 · 1 min · jiezi

一步步教你用HTML5 SVG实现动画效果

翻译:疯狂的技术宅原文:https://www.smashingmagazine….本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜的前端技术文章摘要在这篇文章中你将了解Awwwards网是怎样实现动画的。 本文介绍了HTML5 SVG中的circle 元素,它的stroke属性,以及如何使用CSS变量以及用 Vanilla JavaScript 为它们设置动画。SVG是一种基于XML的,用于定义缩放矢量图形的标记语言。 它允许你通过在2D平面中确定的一组点来绘制路径、曲线和形状。 此外你还可以通过在这些路径上添加动态属性(例如笔触,颜色,粗细,填充等)来生成动画。从2017年4月起,CSS Level 3 填充和描边模块开始支持从外部样式表设置SVG颜色和填充图案,而不是在每个元素上设置属性。 在本教程中,我们将会使用简单的纯十六进制颜色,不过填充和描边属性也支持图案,渐变和图像作为值。注意:访问Awwwards网站时,你需要把浏览器宽度设置为1024px或更高的才能更好的查看动画显示。演示链接源代码文件结构让我们从在终端中创建文件开始:???? mkdir note-display???? cd note-display???? touch index.html styles.css scripts.jsHTML这是连接css和js文件的初始模板:<html lang=“en”><head> <meta charset=“UTF-8”> <title>Note Display</title> <link rel=“stylesheet” href="./styles.css"></head><body> <script src="./scripts.js"></script></body></html>每个note元素都包含一个列表项:li用于保存circle,note值及其label。图:列出项元素及其直接子元素:.circle, .percent 和 .label.circle_svg是一个SVG元素,它包含两个 <circle>元素。 第一个是要填充的路径,第二个用来为动画作准备。图:SVG元素:SVG包装器和圆形标签注释分为整数和小数,所以可以把它们设定为不同大小的字体。 label 是一个简单的<span>。 把所有得这些元素放在一起看起来像这样:<li class=“note-display”> <div class=“circle”> <svg width=“84” height=“84” class=“circle__svg”> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–path”></circle> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–fill”></circle> </svg> <div class=“percent”> <span class=“percent__int”>0.</span> <span class=“percent__dec”>00</span> </div> </div> <span class=“label”>Transparent</span></li>cx和cy属性定义圆的x轴和y轴中心点。 r属性定义其半径。你可能已经注意到类名中的下划线/破折号模式。 这是BEM(block element modifier),分别代表 block, element 和 modifier。 它是使元素命名更加结构化、有条理和语义化的一种方法。推荐阅读:什么是BEM以及为什么需要它为了完成模板结构,让我们将四个列表项包装在无序列表元素中:图:无序列表包装器拥有四个li子元素<ul class=“display-container”> <li class=“note-display”> <div class=“circle”> <svg width=“84” height=“84” class=“circle__svg”> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–path”></circle> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–fill”></circle> </svg> <div class=“percent”> <span class=“percent__int”>0.</span> <span class=“percent__dec”>00</span> </div> </div> <span class=“label”>Transparent</span> </li> <li class=“note-display”> <div class=“circle”> <svg width=“84” height=“84” class=“circle__svg”> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–path”></circle> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–fill”></circle> </svg> <div class=“percent”> <span class=“percent__int”>0.</span> <span class=“percent__dec”>00</span> </div> </div> <span class=“label”>Reasonable</span> </li> <li class=“note-display”> <div class=“circle”> <svg width=“84” height=“84” class=“circle__svg”> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–path”></circle> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–fill”></circle> </svg> <div class=“percent”> <span class=“percent__int”>0.</span> <span class=“percent__dec”>00</span> </div> </div> <span class=“label”>Usable</span> </li> <li class=“note-display”> <div class=“circle”> <svg width=“84” height=“84” class=“circle__svg”> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–path”></circle> <circle cx=“41” cy=“41” r=“38” class=“circle__progress circle__progress–fill”></circle> </svg> <div class=“percent”> <span class=“percent__int”>0.</span> <span class=“percent__dec”>00</span> </div> </div> <span class=“label”>Exemplary</span> </li></ul>你必须先问一下自己 Transparent、 Reasonable、 Usable 和 Exemplary 标签都代表什么意思。 随着你对编程的不断熟悉,就会发现写代码不仅仅是为了能够使程序正常运行,还需要要确保它能够被长期维护和扩展。 这些只有在你的代码容易被修改时才能够实现。“缩略词TRUE应该能够帮助你确定自己编写的代码是否能够适应未来的变化。”那么,下次问问你自己:透明:代码更改后果是否明确?合理:成本效益值得吗?可用:我是否能够在意外情况下重复使用它?示例:它是否以高质量作为未来代码的示例?Transparent(透明):代码在修改后果是否明确?Reasonable(合理):成本效益值得吗?Usable(可用):我是否能够在不同的场景下重复使用它?Exemplary(示例):未来它是否可以作为高质量作为代码范本?注:Sandi Metz在《面向对象设计实践指南:Ruby语言描述》一书解释了TRUE和其他原则,以及如何通过设计模式实现它们。 如果你还没有开始研究设计模式,请考虑将此书放到自己的案头。CSS让我们导入字体并使其对所有内容生效:@import url(‘https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200’);* { padding: 0; margin: 0; box-sizing: border-box;}box-sizing: border-box 属性中包括填充与边框值到元素的总宽度和高度,所以更容易计算图形的范围。注意:有关 box-sizing的说明,请阅读“使用CSS Box让你更轻松”_。body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: ‘Nixie One’, cursive;}.display-container { margin: auto; display: flex;}通过组合规则显示:body 中的 flex 和 .display-container 中的 margin-auto,可以将子元素垂直水平居中。 .display-container元素也将作为一个 flex-container; 这样,它的子元素会沿主轴被放置在同一行。.note-display 列表项也将是一个 flex-container。 由于有很多子项被居中,所以我们可以通过 justify-content 和 align-items 属性来完成。 所有 flex-items 都将垂直水平居中。 如果你不确定它们是什么,请查看“CSS Flexbox 可视化指南”中的对齐部分。.note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px;}让我们通过设置stroke-width,stroke-opacity 和 stroke-linecap 将笔划应用于圆,这些规则会使画面动起来。 接下来,我们为每个圆添加一种颜色:.circle__progress { fill: none; stroke-width: 3; stroke-opacity: 0.3; stroke-linecap: round;}.note-display:nth-child(1) .circle__progress { stroke: #AAFF00; }.note-display:nth-child(2) .circle__progress { stroke: #FF00AA; }.note-display:nth-child(3) .circle__progress { stroke: #AA00FF; }.note-display:nth-child(4) .circle__progress { stroke: #00AAFF; }为了绝对定位百分比元素,必须完全知道这些概念是什么。 .circle元素应该是引用,所以让我们为其添加添加 position: relative 。注意:对绝对定位更深入、直观的解释,请阅读“一劳永逸的理解 CSS Position”一文。另一种使元素居中的方法是把 top: 50%, left: 50% 和 transform: translate(-50%, -50%); 组合在一起, 将元素的中心定位在其父级中心。.circle { position: relative;}.percent { width: 100%; top: 50%; left: 50%; position: absolute; font-weight: bold; text-align: center; line-height: 28px; transform: translate(-50%, -50%);}.percent__int { font-size: 28px; }.percent__dec { font-size: 12px; }.label { font-family: 'Raleway', serif; font-size: 14px; text-transform: uppercase; margin-top: 15px;}到目前为止,模板应如该是下面这个样子:图:完成的模板元素和样式填充过渡可以在两个圆形SVG属性的帮助下创建圆形动画:stroke-dasharray 和 stroke-dashoffset。“stroke-dasharray 定义笔划中的虚线间隙模式。”它最多可能需要四个值:当它被设置为唯一的整数( stroke-dasharray:10 )时,破折号和间隙具有相同的大小;对于两个值( stroke-dasharray:10 5 ),第一个应用于破折号,第二个应用于间隙;第三种和第四种形式(stroke-dasharray:10 5 2 和 stroke-dasharray:10 5 2 3 )将产生各种样式的虚线和间隙。图:stroke-dasharray属性值左边的图像显示属性stroke-dasharray设置为 0 到圆周长度 238px。第二个图像表示 stroke-dashoffset 属性,它抵消了dash数组的开头。 它的取值范围也是从0到圆周长度。图:stroke-dasharray 和 stroke-dashoffset 属性为了产生填充效果,我们将 stroke-dasharray 设置为圆周长度,以便它所有长度都能充满其冲刺范围而不留间隙。 我们也会用相同的值抵消它,这样会使它能够被“隐藏”。 然后,stroke-dashoffset 将更新为对应的说明文字,根据过渡持续时间填充其行程。属性更新将通过CSS Variables在脚本中完成。 下面让我们声明变量并设置属性:.circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease;}为了设置初始值并更新变量,让我们从使用 document.querySelectorAll 选择所有.note-display元素开始。 同时把 transitionDuration设置为900毫秒。然后,我们遍历显示数组,选择它的 .circle__progress.circle__progress--fill 并提取HTML中的 r 属性集来计算周长。 有了它,我们可以设置初始的 --dasharray 和 --dashoffset 值。当 --dashoffset 变量被 setTimeout 更新时,将发生动画:const displays = document.querySelectorAll('.note-display');const transitionDuration = 900;displays.forEach(display =&gt; { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; progress.style.setProperty('--transitionDuration', ${transitionDuration}ms); progress.style.setProperty('--initialStroke', circumference); setTimeout(() =&gt; progress.style.strokeDashoffset = 50, 100);});要从顶部开始过度,必须旋转 .circle__svg 元素:.circle__svg { transform: rotate(-90deg);}图:Stroke 属性转换现在,让我们计算相对于 note 的dashoffset值。 note 值将通过 data-* 属性插入每个li项目。 * 可以替换为任何符合你需求的名称,然后可以通过元素的数据集在元数据集中检索:element.dataset.*。注意:你可以在MDN Web Docs上得到有关 data-* 属性的更多信息。我们的属性将被命名为 “data-note”:&lt;ul class="display-container"&gt;+ &lt;li class="note-display" data-note="7.50"&gt; &lt;div class="circle"&gt; &lt;svg width="84" height="84" class="circle__svg"&gt; &lt;circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"&gt;&lt;/circle&gt; &lt;circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"&gt;&lt;/circle&gt; &lt;/svg&gt; &lt;div class="percent"&gt; &lt;span class="percent__int"&gt;0.&lt;/span&gt; &lt;span class="percent__dec"&gt;00&lt;/span&gt; &lt;/div&gt; &lt;/div&gt; &lt;span class="label"&gt;Transparent&lt;/span&gt; &lt;/li&gt;+ &lt;li class="note-display" data-note="9.27"&gt; &lt;div class="circle"&gt; &lt;svg width="84" height="84" class="circle__svg"&gt; &lt;circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"&gt;&lt;/circle&gt; &lt;circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"&gt;&lt;/circle&gt; &lt;/svg&gt; &lt;div class="percent"&gt; &lt;span class="percent__int"&gt;0.&lt;/span&gt; &lt;span class="percent__dec"&gt;00&lt;/span&gt; &lt;/div&gt; &lt;/div&gt; &lt;span class="label"&gt;Reasonable&lt;/span&gt; &lt;/li&gt;+ &lt;li class="note-display" data-note="6.93"&gt; &lt;div class="circle"&gt; &lt;svg width="84" height="84" class="circle__svg"&gt; &lt;circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"&gt;&lt;/circle&gt; &lt;circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"&gt;&lt;/circle&gt; &lt;/svg&gt; &lt;div class="percent"&gt; &lt;span class="percent__int"&gt;0.&lt;/span&gt; &lt;span class="percent__dec"&gt;00&lt;/span&gt; &lt;/div&gt; &lt;/div&gt; &lt;span class="label"&gt;Usable&lt;/span&gt; &lt;/li&gt;+ &lt;li class="note-display" data-note="8.72"&gt; &lt;div class="circle"&gt; &lt;svg width="84" height="84" class="circle__svg"&gt; &lt;circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"&gt;&lt;/circle&gt; &lt;circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"&gt;&lt;/circle&gt; &lt;/svg&gt; &lt;div class="percent"&gt; &lt;span class="percent__int"&gt;0.&lt;/span&gt; &lt;span class="percent__dec"&gt;00&lt;/span&gt; &lt;/div&gt; &lt;/div&gt; &lt;span class="label"&gt;Exemplary&lt;/span&gt; &lt;/li&gt;&lt;/ul&gt;parseFloat方法将display.dataset.note返回的字符串转换为浮点数。 offset 表示达到最高值时缺失的百分比。 因此,对于 7.50 note,我们将得到 (10 - 7.50) / 10 = 0.25,这意味着 circumference 长度应该偏移其值的25%:let note = parseFloat(display.dataset.note);let offset = circumference * (10 - note) / 10;更新scripts.js:const displays = document.querySelectorAll('.note-display');const transitionDuration = 900;displays.forEach(display =&gt; { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius;+ let note = parseFloat(display.dataset.note);+ let offset = circumference * (10 - note) / 10; progress.style.setProperty('--initialStroke', circumference); progress.style.setProperty('--transitionDuration', ${transitionDuration}ms);+ setTimeout(() =&gt; progress.style.strokeDashoffset = offset, 100);});sroke属性转换为note值在继续之前,让我们将stoke转换提取到它自己的方法中:const displays = document.querySelectorAll('.note-display');const transitionDuration = 900;displays.forEach(display =&gt; {- let progress = display.querySelector('.circle__progress--fill');- let radius = progress.r.baseVal.value;- let circumference = 2 * Math.PI * radius; let note = parseFloat(display.dataset.note);- let offset = circumference * (10 - note) / 10;- progress.style.setProperty('--initialStroke', circumference);- progress.style.setProperty('--transitionDuration', ${transitionDuration}ms);- setTimeout(() =&gt; progress.style.strokeDashoffset = offset, 100);+ strokeTransition(display, note);});+ function strokeTransition(display, note) {+ let progress = display.querySelector('.circle__progress--fill');+ let radius = progress.r.baseVal.value;+ let circumference = 2 * Math.PI * radius;+ let offset = circumference * (10 - note) / 10;+ progress.style.setProperty('--initialStroke', circumference);+ progress.style.setProperty('--transitionDuration', ${transitionDuration}ms);+ setTimeout(() =&gt; progress.style.strokeDashoffset = offset, 100);+ }注意增长值还有一件事就是把 note 从0.00转换到要最终的 note 值。 首先要做的是分隔整数和小数值。 可以使用字符串方法split()。 之后它们将被转换为数字,并作为参数传递给 increaseNumber() 函数,通过整数和小数的标志正确显示在对应元素上。const displays = document.querySelectorAll('.note-display');const transitionDuration = 900;displays.forEach(display =&gt; { let note = parseFloat(display.dataset.note);+ let [int, dec] = display.dataset.note.split('.');+ [int, dec] = [Number(int), Number(dec)]; strokeTransition(display, note);+ increaseNumber(display, int, 'int');+ increaseNumber(display, dec, 'dec');});在 increaseNumber() 函数中,我们究竟选择 .percent__int 还是 .percent__dec 元素,取决于 className ,以及输出是否应包含小数点。 接下来把transitionDuration设置为900毫秒。 现在,动画表示从0到7的数字,持续时间必须除以note 900 / 7 = 128.57ms。 结果表示每次增加迭代将花费多长时间。 这意味着 setInterval将每隔 128.57ms 触发一次。设置好这些变量后,接着定义setInterval。 counter 变量将作为文本附加到元素,并在每次迭代时增加:function increaseNumber(display, number, className) { let element = display.querySelector(.percent__${className}), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() =&gt; { element.textContent = counter + decPoint; counter++; }, interval);}图:计数增长太酷了! 确实增加了计数值,但它在无限循环播放。 当note达到我们想要的值时,还需要清除setInterval。 可以通过clearInterval函数完成:function increaseNumber(display, number, className) { let element = display.querySelector(.percent__${className}`), decPoint = className === ‘int’ ? ‘.’ : ‘’, interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => {+ if (counter === number) { window.clearInterval(increaseInterval); } element.textContent = counter + decPoint; counter++; }, interval);}图:最终完成现在,数字更新到note值,并使用clearInterval()函数清除。教程到此就结束了,希望你能喜欢它!如果你想开发一些更具互动性的东西,请查看使用 Vanilla JavaScript 创建的Memory Game Tutorial 。 它涵盖了基本的HTML5,CSS3和JavaScript概念,如定位、透视、转换、Flexbox、事件处理、超时和三元组。祝你快乐的编码!????本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜的前端技术文章 ...

January 22, 2019 · 5 min · jiezi