引言

在几年前,我就在一些博客中看到关于CSS中transform的分析,讲到它与线性代数中矩阵的关系,但当时由于使用transform比较少,再加上我毕竟是个数学学渣,对数学有点畏难心理,就有点看不下去,所以只是随便扫了两眼,就没有再继续了解了。现在在学习可视化,又遇到了这个点,又说到这是可视化的基础知识,既然这样,那看来还是逃不过去,那就再多了解一点吧。

transform的作用

使用过transform的前端小伙伴一定不陌生,通过对CSS中transform属性的设置,我们可以对DOM元素进行缩放、旋转、平移,以及扭曲,从而改变元素的位置、形状、大小和角度。

仿射变换

CSS中的transform对应到图形学中的概念就是仿射变换。

仿射变换简单来说就是“线性变换 + 平移”。

在CSS中对某个DOM元素应用仿射变换,可以简单理解成是把这个元素原本的整个坐标系进行了变换,并且这个坐标系的原点在最初始时位于DOM元素的中心,X轴朝右、Y轴朝上、Z轴朝外,也就是朝向屏幕。

所以就是说,对某个DOM元素进行仿射变换,就相当于对它所对应的几何图形的每个顶点向量进行仿射变换。

关于图形的仿射变换,有两个性质:

第一,仿射变换不改变直线段的形状,也就是说,应用仿射变换后,直线段依旧是直线段;

第二,应用相同的仿射变换后,两条直线段的长度比例保持不变。

平移

接下来我们先说平移,平移变换是最简单的仿射变换。

假设存在一个向量P(x0, y0),我们想要把它沿着另一个向量Q(x1, y1)的方向移动对应距离,那只要将两个向量相加,我们就可以得到这个新的向量它的坐标。

x = x0 + x1
y = y0 + y1

这就是平移变换的公式。

线性变换

根据公式可以看出,应用平移变换后,原始坐标系的原点会发生变化。

但是应用线性变换后,原点却并不会变化;下面来讲解两个常用的线性变换:旋转和缩放。

  • 旋转

    首先我们先来看旋转变换。

    假设存在一个向量P(x0, y0),长度为r,与X轴夹角为θ,现在将它逆时针旋转α角,那么此时新的向量P'的坐标x和y分别是多少呢?

    首先我们根据圆的参数方程,可以得到如下公式:

    x0 = r * cosθ
    y0 = r * sinθ x = r * cos(α+θ)
    y = r * sin(α+θ)

    但这样并看不出新旧坐标之间的关联,所以需要进行推导。

    在上图中,我们假设旋转θ角后得到了一个新的坐标系(蓝色),此时我们可以求得向量P'在新坐标系的坐标,此时P'在新坐标系的坐标可以表示为:

    x' = r * cosα -> AF
    y' = r * sinα -> AI

    分别相当于是线段AF和AI的长度。

    此时依旧看不出新旧坐标之间的关联,我们还需要继续推导,求出向量P'在原坐标系的值,在上图中相当于我们要求出线段AJ和AK的长度。

    • 先来求AJ的长度

      首先我们从图中可以看出AJ = AG - JG, 并且 AG = AF * cosθ;

      同时JG 和LF的长度相同,DF与AI的长度相同,且角FDJ的度数也是θ,所以可以得到JG = AI * sinθ。

      最终我们可以得到如下公式:

      AJ = AF * cosθ - AI * sinθ
      = r * cosα * cosθ - r * sinα * sinθ

      又因为:

      x0 = r * cosθ
      y0 = r * sinθ

      就可以得到AJ的长度,也就是新向量的x坐标

      x = x0 * cosα - y0 * sinα
    • 接着来求AK的长度

      从图中我们也可以看出AK = AM + MK,并且AM = AI * cosθ

      MK又可以分为MN和NK两段,相当于MK = AF * sinθ。

      最终我们可以得到:

      AK = AI * cosθ + AF * sinθ
      = r * sinα * cosθ + r * cosα * sinθ

      再加上原坐标和角度及半径的关系,就可以得到AK的长度,也就是新向量的y坐标:

      y = x0 * sinα + y0 * cosα

    至此我们就得到了新坐标和原坐标以及旋转角度之间的关系,也就是旋转变换的公式:

    x = x0 * cosα - y0 * sinα
    y = x0 * sinα + y0 * cosα

    根据线性代数的知识,我们可以使用矩阵的形式来表示以上公式:

    [x]   [cosα  -sinα]   [x0]
    | | = | | x | |
    [y] [sinα cosα] [y0]
  • 缩放

    接着我们继续看缩放变换。缩放变换相当于是让向量与标量相乘。

    比如我们使X轴缩放比例为sx,使Y轴缩放比例为sy,就可以得到新向量的坐标为:

    x = sx * x0
    y = sy * y0

    缩放比旋转简单一些,可以直接写出矩阵形式的公式:

    [x]   [sx  0]   [x0]
    | | = | | x | |
    [y] [0 sy] [y0]

至此,我们就基本了解了仿射变换的公式,并且可以看出线性变换的公式可以用矩阵相乘的形式进行表示。

除了不改变原点,线性变换还有另外一个性质,就是可以进行叠加;多个线性变换的叠加结果就是将线性变换的矩阵依次相乘,最后再与原始向量相乘。

根据以上内容,我们可以得到仿射变换的一般表达式:

P = M x P0 + P1

M为多个线性变换的叠加结果,也就是变换矩阵的相乘结果,P0为原始向量坐标,P1为平移。

公式优化

为了便于计算,我们还可以对以上的仿射变换表达式进行优化,通过增加维度来使用矩阵进行表示:

[P]   [M  P1]   [P0]
| | = | | x | |
[1] [0 1] [1 ]

这实际上就是给线性空间增加了一个维度,用高维度的线性变换表示了低维度的仿射变换。

这种n+1维坐标被称为齐次坐标,对应的矩阵被称为齐次矩阵。

我们需要注意,由于平移变换会改变坐标原点,不同的变换顺序很可能会导致不同的变换结果,所以要注意矩阵相乘的顺序。

公式应用

接下来我们就来应用一下线性变换的公式。

假设现在在页面上有一个div。

<div class="block separate">我使用分开写</div>
.block {
width: 100px;
height: 100px;
color: #fff;
background: orange; &.separate {
transform: rotate(30deg) translate(100px, 50px) scale(1.5);
}
}

通过简单的旋转和平移,我们改变了元素的角度、位置和大小。

此时我们对于transform的变换是分开写的,但在CSS的transform中,可以使用一个matrix函数,让我们对这些变换进行合并编写。

首先我们引入一个ogl库,使用其中定义的矩阵类Mat3(也可以借助其他数学库,比如mathjs):

import { Mat3 } from 'ogl';

然后针对上面的3个变换,分别定义三个变换矩阵,分别是旋转矩阵、平移矩阵和缩放矩阵:

const rad = Math.PI / 6;

let a = new Mat3(
// 旋转矩阵
Math.cos(rad), -Math.sin(rad), 0,
Math.sin(rad), Math.cos(rad), 0,
0, 0, 1
);
let b = new Mat3(
// 平移矩阵
1, 0, 100,
0, 1, 50,
0, 0, 1
);
let c = new Mat3(
// 缩放矩阵
1.5, 0, 0,
0, 1.5, 0,
0, 0, 1
); // -------------
// 使用math.js
const a = math.matrix(
[
[Math.cos(rad), -Math.sin(rad), 0],
[Math.sin(rad), Math.cos(rad), 0],
[0, 0, 1]
]
);
const b = math.matrix(
[
[1, 0, 100],
[0, 1, 50],
[0, 0, 1]
]
);
const c = math.matrix(
[
[1.5, 0, 0],
[0, 1.5, 0],
[0, 0, 1]
]
);

接着对三个矩阵进行相乘,得到axbxc的结果:

const res = [a, b, c].reduce((prev, current) => {
return current.multiply(prev); // prev x current 结果保存到current
}); // -------------
// 使用math.js
let res = math.multiply(a, b);
res = math.multiply(res, c);

最后我们利用CSS变量将JS的计算结果应用到样式上:

.block {
// ... &.combine {
--trans: none;
transform: var(--trans);
}
}

由于CSS的matrix是一个简写的齐次矩阵,它省略了三阶齐次矩阵第三行的0,0,1,所以只有6个值。

const combine = document.querySelector('.combine');
const s = res.slice(0, 6);

matrix貌似是列主序,所以在设置的时候,需要按如下顺序赋值:

const combine = document.querySelector('.combined');

combine.style.setProperty('--trans', `matrix(
${s[0]},${s[3]},
${s[1]},${s[4]},
${s[2]},${s[5]},
)`); // -------------
// 使用math.js
const s = Array.from(res).map(item => item.value);
combine.style.setProperty('--trans', `matrix(
${s[0]},${s[3]},
${s[1]},${s[4]},
${s[2]},${s[5]}
)`);

可以明显看出,这样使用的效果,和rotate、translate和scale分开写的效果是一样的。

总结

利用仿射变换,我们可以快速绘制出形态、位置、大小各异的众多几何图形,比如实现粒子动画。

也许在普通的前端开发中,用不到太多,也并不太需要说去利用matrix去减少CSS的代码体积,但如果要去做可视化方面的开发,仿射变换还是可以多去了解一下。

可视化学习:CSS transform与仿射变换的更多相关文章

  1. No.3 - CSS transition 和 CSS transform 配合制作动画

    课程概述 作业提交截止时间:09-01 任务目的 深度理解掌握 transition-timing-function 以及它的意义 学会配合使用 CSS transform 和CSS transiti ...

  2. CSS Transform完全指南 #flight.Archives007

    Title/ CSS Transform完全指南 #flight.Archives007 序: 第7天了! 终身学习, 坚持创作, 为生活埋下微小的信仰. 我是忘我思考,共同进步! 简介: 一篇最简约 ...

  3. CSS Transform完全指南(第二版) #flight.Archives007

    Title/ CSS Transform完全指南(第二版) #flight.Archives007 序: 第7天了! 终身学习, 坚持创作, 为生活埋下微小的信仰. 我是忘我思考,共同进步! 简介: ...

  4. 学习 CSS 样式

    1.CSS浮动  :  http://www.cnblogs.com/zhongxinWang/archive/2013/03/27/2984764.html (1)一个重要结论:           ...

  5. css知多少(2)——学习css的思路

    两周之前写过该系列的第一篇,其实当时只是一个想法,这段时间迟迟未更新,是在思考一个解决过程.现在初有成效,就开更吧. 1. 一个段子 开题不必太严肃,写博客也不像写书,像聊天似的写东西是最好的表达方式 ...

  6. Tensorflow学习笔记3:TensorBoard可视化学习

    TensorBoard简介 Tensorflow发布包中提供了TensorBoard,用于展示Tensorflow任务在计算过程中的Graph.定量指标图以及附加数据.大致的效果如下所示, Tenso ...

  7. 开始学习css

    今天开始学习css:应用一本<HTML5与CSS3网页设计基础> 先学习css样式规则声明. Body{ color:blue} 对应:选择符:{声明属性:声明值}: Background ...

  8. HTML+CSS学习笔记 (6) - 开始学习CSS

    HTML+CSS学习笔记 (6) - 开始学习CSS 认识CSS样式 CSS全称为"层叠样式表 (Cascading Style Sheets)",它主要是用于定义HTML内容在浏 ...

  9. 如何深入学习CSS

    学习CSS有了一定基础后,有的人会觉得好象没有什么学的.因为知道一些基本的理论性的东西.CSS说它容易是因为它的知识点有限.说它难学就在于各浏览器对CSS的支持程度不同.如何深入学习我给出以下几点见意 ...

  10. 【图片版】学习CSS网格布局

    简言 CSS网格布局(Grid)是一套二维的页面布局系统,它的出现将完全颠覆页面布局的传统方式.传统的CSS页面布局 一直不够理想.包括table布局.浮动.定位及内联块等方式,从本质上都是Hack的 ...

随机推荐

  1. LeetCode 周赛上分之旅 #44 同余前缀和问题与经典倍增 LCA 算法

    ️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问. 学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越 ...

  2. 2.9 PE结构:重建导入表结构

    脱壳修复是指在进行加壳保护后的二进制程序脱壳操作后,由于加壳操作的不同,有些程序的导入表可能会受到影响,导致脱壳后程序无法正常运行.因此,需要进行修复操作,将脱壳前的导入表覆盖到脱壳后的程序中,以使程 ...

  3. DHorse v1.4.0 发布,基于 k8s 的发布平台

    版本说明 新增特性 提供Fabric8客户端操作k8s(预览)的功能,可以通过指定-Dkubernetes-client=fabric8参数开启: Vue.React应用增加Pnpm.Yarn的构建方 ...

  4. VMware Work Station使用ubuntu20.04挂载共享文件夹写入文件时出现输入/输出错误

    原因是默认的max_write为0x00020000即128k,超过此大小会报错,另外big_writes,umask等选项也要加上, sudo /usr/bin/vmhgfs-fuse .host: ...

  5. 前端框架——Vue2

    文章目录 初识Vue 模板语法 数据绑定 el与data的两种写法 理解MVVM 数据代理 事件处理 计算属性 监视属性 绑定样式 条件渲染 列表渲染 收集表单数据 过滤器 内置指令 自定义指令 生命 ...

  6. Go 基础之基本数据类型

    Go 基础之基本数据类型 目录 Go 基础之基本数据类型 一.整型 1.1 平台无关整型 1.1.1 基本概念 1.1.2 分类 有符号整型(int8~int64) 无符号整型(uint8~uint6 ...

  7. vim vimtutor

    =============================================================================== =      歡     迎     閱 ...

  8. 教育法学第六章单元测试MOOC

    第六章单元测试 返回 本次得分为:100.00/100.00, 本次测试的提交时间为:2020-09-06, 如果你认为本次测试成绩不理想,你可以选择 再做一次 . 1 单选(5分) "学习 ...

  9. C++在HotSpot VM中一种巧妙的内存管理方式

    在HotSpot VM中定义了一个Relocation类及相关的子类,可以通过这些类操作不同的重定位数据,如在CodeCache中读写这些数据.这些类需要的内存很小,但是不同的类需要的内存大小又不一样 ...

  10. AT通讯总结(56K猫调制解调器Modem)型号I-56EM

    1.关闭流控RTS与DTR AT&D0&K0\r\n 2.保存到非易失性存储 AT&W\r\n 3.向800001音频拨号 ATDT800001\r\n 4.接听 ATA\r\ ...