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站:

Logo小变动,心境大不同,SVG矢量动画格式网站Logo图片制作与实践教程(Python3)_第1张图片

总的来说,采用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矢量图:

Logo小变动,心境大不同,SVG矢量动画格式网站Logo图片制作与实践教程(Python3)_第2张图片

注意图像颜色位数最好越小越好,这样转化后的图像不会过大,同时背景最好是透明的,因为透明元素不会被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 """  
  
  
""" % (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(  
                """  \n"""  
                % (x, y, rgba[0:3], float(rgba[3]) / 255))  
        print("Converting pixels: " + str(x * 100 / width) + "%")  
    s.write("""\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(""" \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("""\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))

运行脚本文件:

python3 test.py test.png

将会直接产出同名的test.svg文件,利用vscode(安装svg预览控件)打开:

Logo小变动,心境大不同,SVG矢量动画格式网站Logo图片制作与实践教程(Python3)_第3张图片

原理就是将原位图的像素进行遍历,转化成SVG的Path路径格式,每个位图元素块都会是单独的一个Path标签,如果不需要对某个单独元素块进行操作,也可以合并为一个path路径标签。

接下来,将转化好的svg文件进行压缩,压缩后体积会相应的减小,同时一些path也可以被合并,通过npm安装压缩软件svgo:

npm install -g svgo

执行压缩命令,test.svg是源文件,my.svg是压缩后的文件:

svgo test.svg -o my.svg

得到压缩后的svg:

  
   
      
  

这里我们将多个path路径合并为一个,如果不想使用本地的代码脚本进行转化和压缩,也可以利用线上对应的网络工具,转化svg:https://www.pngtosvg.com/ 压缩svg: https://jakearchibald.github....

线上转化效果和本地转化效果大同小异。

SVG交互效果

事实上,直接将上面的代码放到页面中,就可以直接使用了,这里通过指定svg的viewBox属性里面的4个坐标来标定 svg 图片的显示区域,也就是我们理解的“画布”,然后通过控制width属性来实现等比的扩大和缩小,非常方便,效果是这样的:

Logo小变动,心境大不同,SVG矢量动画格式网站Logo图片制作与实践教程(Python3)_第4张图片

除了分辨率能够适应屏幕显得更加精细,其他的地方和传统png好像没有什么不同?那么就来点不一样的吧,svg可以通过css样式来实现“动画”特效,比如描边:

  
   
      
  

这里我们为path属性加上伪类,同时通过stroke属性为logo加上一层外边框,透明度为0.2,宽度为7个像素,接着,编写关键帧动画:

@keyframes pinap {  
  to {  
    stroke-dashoffset: 0;  
  }  
  0% {fill-opacity: 0; }  
   
    100% {fill-opacity: 1;    filter: drop-shadow(0px 0px 2px var(--logo-color));}   
  
}

页面加载时,指定 stroke-dasharray 属性进行描边的路径遍历,同时将logo本体的颜色透明度由0至1进行递增操作:

Logo小变动,心境大不同,SVG矢量动画格式网站Logo图片制作与实践教程(Python3)_第5张图片

如果觉得意犹未尽,可以利用hover属性,将鼠标对Logo进行悬停动作时再触发一遍特效:

path.path:hover{  
stroke-dasharray: 1000;  
  stroke-dashoffset: 1000;  
  animation: dash 3s linear forwards;  
  /* fill:white; */  
  fill-opacity: 1;  
}

还记得之前对于网站“暗黑模式”的实验吗?利用CSS3自定义属性来为网站添加“暗黑模式”(暗色模式/DarkMode) 当时我们使用了自定义属性,切换暗黑模式的同时切换另外一张暗色的Logo达到明亮和暗黑的效果,成本就是需要一明一暗两张图片配合使用,而现在,不需要多余的图片,只需要通过简单的自定义颜色即可:

:root{--logo-color:#2b2b2b}  
  
path.path{  
    fill:var(--logo-color);  
}

svg的fill属性可以直接渲染图片颜色:

Logo小变动,心境大不同,SVG矢量动画格式网站Logo图片制作与实践教程(Python3)_第6张图片

简单而又方便,还等什么?抛弃落伍的PNG吧。

配合滤镜,还可以打造霓虹灯特效:

filter: drop-shadow(0px 0px 2px var(--logo-color));

Logo小变动,心境大不同,SVG矢量动画格式网站Logo图片制作与实践教程(Python3)_第7张图片

结语:“旧时王谢堂前燕,飞入寻常百姓家”,通过淘汰落后产能、上大压小、加强技术革新,我们可以利用SVG技术打造成本更低,扩展性更高的图像展示方案,何乐而不为?最后,奉上演示项目地址:https://github.com/zcxey2911/svg\_website\_logo,博君一晒。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_207

你可能感兴趣的:(Logo小变动,心境大不同,SVG矢量动画格式网站Logo图片制作与实践教程(Python3))