{T}

数学的魔法:探索数学在动画中的应用

在数字化的时代,数学不再是一种抽象的理论,而是动画创造中的实用魔法。因为动画是一种模拟运动的视觉过程,它关乎于时间和比例,这与数学有着紧密的联系。尽管在动画制作中通常强调创造力,但这离不开数学,只有与数学魔力结合在一起,才能创造出更杰出的动画效果。

Web 开发者或设计师在创造动画时需要各种数学技术,包括几何学和线性代数等。因为在处理移动和变化的对象时,这使 Web 开发者或设计师能够解决几何问题,并从一组简单的方程中识别未知数。这意味着,为了能创造出更优秀的动画,我们需要对应用于动画中的数学知识要有基本的理解。然而,当考虑到几何学、三角学、线性代数和微积分等背后使用的复杂概念时,将会使“数学在动画中的应用”变得更加复杂。

在这节课中,我并不能将几何学、三角学和线性代数等背后的复杂概念阐述清楚,但在这里将会以实际的案例向大家揭示数学是如何成为动画创作的灵魂,鼓励大家深入剖析数学在动画中的神奇运用,发现其中蕴含的创意和无限可能。这节课不仅仅是技术的传授,更是一场寻找数字背后创意之源的冒险之旅。

动画中的数学基础

从表面上看,我们是在通过相关的 Web 技术,比如 CSS 、SVG 和 JavaScript 来制作 Web 动画,整个过程就是处理物体(元素)的移动和变化,比如几何变换(平移、旋转、缩放等)、曲线和路径、插值计算、色彩与光影、投影与视角和物理模拟(牛顿力学和粒子系统)等。Web 开发者和设计师要处理好这些,那都离不开数学。例如,动画中的时间与速度就离不开数学知识,因为我们只有利用好相关的数学知识,才能为动画提供更合理的时间与速度,使动画更自然。另外,我们在处理动画元素的外形和位置时,也需要用到一些数学知识,比如通过三角函数来确定元素的位置、距离等。可以说,数学在动画中无处不在。

动画中的几何知识

在动画制作中,几何知识是至关重要的,它提供了对物体形状、位置和运动的深刻理解。例如,Web 开发者可以使用圆圈、正方形和三角形构建动画元素的基本形状,然后通过操纵它们来创造最终的动画效果。通过几何知识的巧妙运用,动画元素的外观变得清晰而平衡,使得整个动画更加生动和有趣。

Demo 地址:https://codepen.io/raymondyang/full/eWGewE (来源于 @Ray

以下是一些可用于动画制作的几何知识:

  • 角色设计(基本几何形状) :动画师使用基本的几何形状,如圆、矩形、三角形,作为角色和物体的基础结构。 这些形状作为构建更复杂图形的基本单元,为动画提供了可塑性和创造性。
  • 物体的变换和动画:几何学中的变换,如平移、旋转和缩放,用于在动画中实现物体的运动和变化。这些变换使得动画师能够控制物体在场景中的位置、方向和尺寸,实现平滑的运动效果。
  • 三维几何学:在三维动画中,几何学和线性代数用于描述和变换三维空间中的物体,允许动画师创造出更真实、具有深度感的场景,增强用户的视觉体验。
  • 几何构建与分解:将复杂的物体通过基本几何形状的组合构建,或者反之,将复杂的形状分解为简单的几何元素。这种构建和分解的技巧使得动画师能够更有效地处理和控制复杂的物体结构。
  • 路径和运动轨迹:动画师使用几何学中的路径和曲线来定义物体在动画中的运动轨迹。这包括贝塞尔曲线等数学工具。几何学的运用使得物体的运动轨迹更加自然和艺术化,为动画增添了生动感。
  • 光影和投影:几何学原理用于计算光影、阴影和物体的投影效果。正确的光影和投影使得动画场景更具真实感,增加了画面的逼真度。
  • 刚体运动和碰撞检测:几何知识用于描述刚体的运动和进行碰撞检测,确保物体之间的交互看起来自然。正确处理物体的运动和碰撞是制作真实感动画的关键,几何知识在这方面发挥着重要作用。

综合而言,几何知识为动画制作提供了强大的工具,使动画师能够更好地理解和控制物体的形状、运动和外观,从而创造出富有表现力和引人入胜的动画作品。

动画中的三角函数

在动画制作中,动画元素可以以各种方式移动,这要归功于动画师对三角函数的理解。例如,动画师可以使用数学概念将动画元素旋转 180 度或 360 度。动画师使用几何和三角函数的组合可以使物体的运动产生幻觉,同时还能够动画师能够创造出各种自然而流畅的动画效果。下面这个摩天轮中每个吊蓝的位置就是使用正弦和余弦函数来计算的:

Demo 地址:https://codepen.io/haoyang/full/dyjVgBN (来源于 @Chuan shi

在以下这些场景中,我们可能会用到三角函数:

  • 平滑的运动和旋转: 正弦和余弦函数常用于产生平滑的运动和旋转效果。这种应用使得动画中的对象能够以自然的方式移动和旋转,避免了突兀和生硬的效果。
  • 摆动和振动效果: 正弦函数广泛用于模拟物体的摆动和振动,例如钟摆的摆动或头部的摇晃。通过调整振幅和频率,动画师可以创造出逼真的摆动效果,增加动画的真实感和趣味性。
  • 路径和轨迹的生成: 三角函数可用于生成复杂的路径和轨迹,如圆周运动、椭圆路径等。这种应用使得动画元素可以按照预定的轨迹运动,创造出各种独特的动画效果,为场景增添变化和复杂性。
  • 变换效果: 正弦和余弦函数可应用于几何变换,如缩放、平移等,用于创建特殊的动画效果。通过变换,动画师可以实现对象在动画中的变化,例如放大缩小、折叠展开等,为场景增色添彩。
  • 动画过渡和淡入淡出: 三角函数可用于调整动画的过渡和淡入淡出效果,控制透明度的变化。这种应用使得动画中的对象能够平滑地出现或消失,创造出渐变的过渡效果。
  • 声音和光效应的同步: 三角函数可以与声音或光效应的波形同步,实现动画与音频或其他效果的协调。 同步效果增强了动画的视听表现,使得动画更富有动态和感染力。
  • 复杂动画效果的实现: 通过组合和调整多个三角函数,动画师可以实现更复杂的动画效果,如自然界的运动、气象效应等。这种综合应用提供了更多创造性的空间,使得动画能够模拟自然界中各种复杂的现象。
  • 角色肢体的自然运动:利用三角学函数,如正弦和余弦,模拟角色肢体的自然摆动和运动。通过精确计算肢体运动,动画师可以创造出更富有生命力和真实感的角色表现。
  • 动画转场效果:利用角度学,动画师可以实现平滑的转场效果,例如角色从站立到行走的过渡。 这种转场效果增强了场景的连贯性,使得观众更容易理解角色的运动状态变化。

总的来说,动画中的三角函数不仅是基础的数学工具,更是创造动画的艺术元素之一。它们为动画师提供了丰富的选项,使他们能够更灵活地控制物体的运动和外观,创造出丰富多彩、生动有趣的动画作品。

动画中的线性代数

线性代数在动画制作中起到了关键的作用,特别是在创造特效、控制运动以及进行场景变换等方面。以下是线性代数在动画中的应用与作用的一些示例:

  • 特效制作: 动画制作中的特效通常涉及到复杂的图形变换和光影效果。线性代数通过矩阵运算等手段,可以精确控制图像的变形、旋转、缩放等,为特效的制作提供了强大的数学基础。例如,通过矩阵运算,可以实现相机的移动和旋转,以及物体的形变,从而创造出引人注目的特效效果。
  • 相机运动: 在动画中,相机的运动对于营造场景和展示故事情节至关重要。线性代数可以用来描述和控制相机的运动,包括平移、旋转和缩放。通过矩阵变换,可以实现相机在三维空间中的任意运动,为观众呈现出更加生动和引人入胜的画面。
  • 图形变换: 动画中的物体通常需要经历各种图形变换,如平移、旋转、缩放等。线性代数提供了描述和实现这些变换的数学工具。通过矩阵运算,可以精确地控制物体的各种变换,使其在动画中呈现出自然而流畅的运动轨迹。
  • 光影效果: 光影效果对于动画的真实感至关重要。线性代数可以用来计算光源对物体的照射效果,包括阴影、反射和折射等。通过使用线性代数的技术,动画师可以精确地模拟光影效果,使场景看起来更加逼真。
  • 三维空间变换: 在三维动画中,物体的运动和变形通常发生在三维空间中。线性代数提供了描述和计算三维空间变换的工具,包括三维矩阵变换和投影等。这些技术使动画师能够准确地控制物体在三维场景中的各种变化。

综合而言,线性代数在动画制作中是一项不可或缺的数学工具,它为动画师提供了精确控制图形和场景的手段,帮助他们创造出引人入胜、逼真生动的动画作品。

动画中的微积分

微积分在动画制作中有着广泛的应用,主要涉及到场景的优化、运动的模拟、特殊效果的实现等多个方面。以下是微积分在动画中的一些应用:

  • 运动模拟: 微积分用于模拟物体在动画中的运动。通过对位置、速度和加速度等物理量进行微积分运算,动画师可以准确地描述物体的运动轨迹。这包括了线性运动、旋转、缩放等变换,使得动画更加真实和流畅。
  • 光影效果的计算: 微积分可用于计算光线的传播、反射和折射,以及光影效果的优化。通过对光学方程进行微积分运算,动画师能够精确地模拟不同材质表面的反射特性,实现更为逼真的光影效果。
  • 场景优化: 在动画中,复杂场景的构建和渲染可能涉及到大量的几何形状和光照计算。微积分可以用于对这些场景的优化,通过对多维数据进行积分和微分,提高渲染效率和图像质量。
  • 特殊效果的实现: 微积分的概念可以应用于实现各种特殊效果,如液体流动、火焰燃烧等。通过对流体动力学和热力学等物理规律进行微积分建模,动画师可以创造出更为逼真和复杂的特殊效果。
  • 形状变换和插值: 微积分在处理形状变换和动画插值时也发挥着关键作用。通过对形状参数的微积分运算,可以实现形状的平滑变化,使得物体的形态变化更加自然。
  • 动画曲线的设计: 微积分可以用于设计动画中的曲线,包括运动轨迹、颜色变化等。通过对曲线方程进行微积分和积分操作,可以调整动画元素的变化速度和过渡效果。

动画中的物理知识

物理知识在动画制作中发挥着重要的作用,对于创造出更加真实和自然的动画效果至关重要。以下是物理知识在动画中的一些应用和作用:

  • 运动和力学模拟: 动画师可以使用物理知识模拟对象的运动和力学效应。牛顿的运动定律和其他物理学规律可用于计算物体的运动轨迹、速度和加速度,从而使动画中的对象表现得更加真实。
  • 重力和空气阻力: 物理知识帮助动画师模拟重力对物体的影响,使得物体在空中下落的速度和方式看起来符合真实。此外,空气阻力的考虑也可以影响物体的运动轨迹,增加动画的真实感。
  • 碰撞和反弹: 物理知识用于模拟物体之间的碰撞和相互作用。通过考虑碰撞的能量守恒和动量守恒,动画中的物体可以表现出逼真的碰撞和反弹效果。
  • 流体动力学: 对于涉及液体或气体的场景,物理学的流体动力学原理可以用于模拟流体的流动、波浪和涡流,从而创造出逼真的水面、烟雾等效果。
  • 布料模拟: 物理学也应用于模拟布料的行为。通过考虑布料的弯曲、拉伸和重力对其的影响,动画中的服装或其他布料可以呈现出更加真实的运动和形态。
  • 角色动作和关节: 解剖学和生物力学的原理用于模拟角色的骨骼和关节运动。这有助于创造出更具生命力和自然的角色动作,使得角色在运动时看起来更加真实。

总体而言,物理知识的应用使得动画师能够更准确地模拟和呈现现实世界中的物理现象,从而提高动画的逼真程度,让用户更容易沉浸在动画的情节和场景之中。

可以说,在动画的世界中,数学是开启新技能和实现创意突破的关键。正如上面所述,数学在动画的各个方面发挥着至关重要的作用,动画师能够利用数学的力量将他们的技艺推向新的高度。动画师可以使用比例、尺度、时序、物理学等知识,使他们能够创造出带真的动画、模拟动画效果,并设计引人入胜的视觉效果。

注意,我无法在一节课中将所有可用于动画中的数学知识全部向大家阐述,而且有很多知识也并不是我所擅长,为了避免误人子弟,接下来我只与大家一起探讨我更熟悉的,并且可以用于动画中的数学知识。

动画中的线性插值计算

在介绍 CSS 过渡动画和关键帧动画的时候,我就提到过,动画设计软件或客户端(比如浏览器)会自动画状态之间进行插值计算。例如过渡动画,会在开始状态和结束状态之间对过渡属性进行插值计算:

关键帧动画的相邻两个关个关键帧(即 @keyframes 规则中相邻的两个选择器)之间会对关键帧进行插值计算:

是的,它们之间的插值是自动计算的。但我们在制作动画的时候,很多时候需要我们自己对插值进行计算,这样做可以使你的动画变得更流畅和自然。例如,我们在两个状态之间对某个属性值变化时,就可以使用线性插值计算,这是一种相当简单的效果,但如果你将对象从 A 点移动到 B 点,它确实可以显著改善动画的效果。

向大家展示线性插值在动画中的实际应用之前,我想先简单地向大家阐述一下线性插值是什么?

线性插值通常称为 Lerp,它是“Linear Interpolation”的简称,是一种在两个点之间进行线性插值的数学运算。它用于在给定的两个数值之间生成一个新的数值,这个新值位于两个给定值的值线路径上,其位置由插值参数决定。它的计算公式如下所示:

code
lerp(a,b,t) = a + (b − a) * t

其中 a 是起始值,b 是结束值,t 是插值参数,通常在 0 ~ 1 之间。

线性插值在动画开发中有许多应用场景,例如:

  • 运动动画: 在两个关键帧之间平滑移动对象。通过在动画的每一帧之间应用线性插值,可以创建更自然的移动效果。
  • 颜色渐变: 在颜色之间创建平滑的渐变效果。例如,在界面元素的状态变化或过渡中,通过在两种颜色之间进行线性插值,可以实现过渡效果。
  • 相机移动: 在虚拟三维空间中平滑移动相机位置。线性插值可用于创建平滑的相机跟随或切换场景的过渡效果。
  • 缩放效果: 控制对象的缩放级别以创建平滑的缩放动画。通过在缩放系数之间应用线性插值,可以使对象以自然的方式放大或缩小。
  • 透明度过渡: 在图形或界面元素的透明度之间实现平滑的过渡。通过在透明度值之间进行线性插值,可以实现淡入淡出等效果。
  • 路径动画: 在给定路径上平滑移动对象。通过在路径上的点之间应用线性插值,可以创建对象沿路径平稳移动的效果。

这些场景只是线性插值在动画开发中的一些示例,它是实现平滑过渡和自然动画效果的常用工具。例如下面这个鼠标跟随的效果,改变 translateX()translateY() 的值就应用了线性插值的计算:

Demo 地址:https://codepen.io/airen/full/WNPPWWB (来源于 @Chris Caldwell

具体代码如下:

JavaScript
var mouseX = 0;
var mouseY = 0;
var mouseMoved = false;

// LERP || Linear Interpolation
function lerp(A, B, t) {
    // A = Starting position
    // B = Final position
    // t = time or percentage A and B i.e. 0.0 to 1.0
    return A + t * (B - A);
}

function handleMouseMove() {
    mouseMoved = true;
    mouseX = event.clientX;
    mouseY = event.clientY;
}

document.onmousemove = handleMouseMove;

function setTransform(element, dx, dy) {
    var transform = "translateX(" + (dx) + "px) translateY(" + (dy) + "px)";
    element.style.transform = transform;  
}

function followMouse(element, t) {
    // Determine Start Position
    var startX = element.getBoundingClientRect().left;
    var startY = element.getBoundingClientRect().top;
  
    // Determine End Position
    var targetX = mouseX - element.offsetWidth / 2;
    var targetY = mouseY - element.offsetHeight / 2;
  
    // Determine LERP position
    var newX = lerp(startX, targetX, t);
    var newY = lerp(startY, targetY, t);
  
    // Set Position using Transforms
    setTransform(element, newX, newY);
}

// Set Position
function setPosition(element, target) {
    // Determine Position
    var targetX = target.offsetWidth / 2 - element.offsetWidth / 2;
    var targetY = target.offsetHeight / 2 - element.offsetHeight / 2;
  
    // Set Position using Transforms
    setTransform(element, targetX, targetY);  
}

// Animation
window.requestAnimationFrame(draw); 
function draw() {
    if (mouseMoved) {
        followMouse(el1, 0.1);
        followMouse(el2, 0.3);    
    }
    window.requestAnimationFrame(draw); 
}

// Handle Load
window.onload = function() {
    var el1 = document.getElementById("el1");
    var el2 = document.getElementById("el2");
    var viewport = document.body;

    // Set Initial Positions
    setPosition(el1, viewport);
    setPosition(el2, viewport);
};

通过这样做,随着位置和目标之间的距离减小,对象移动的距离变得更小。这意味着对象在靠近目标时会减速,从而创建出一种漂亮的缓动效果。

基于上面的示例,使用同样的方式,调整相应的参数,就可以创造其他的动画效果。例如,通过线性插值来改变重复圆锥渐变的圆心位置,实现另一种鼠标跟随的动画效果。

CSS
@layer demo {
    @property --x {
        syntax: "<length-percentage>";
        initial-value: 0%;
        inherits: false;
    }
    
    @property --y {
        syntax: "<length-percentage>";
        initial-value: 0%;
        inherits: false;
    }
  
    .lerpme {
        background: 
            radial-gradient(white, lightseagreen 80vw), 
            repeating-conic-gradient(at var(--x) var(--y),white 0 10deg, lightseagreen 0 20deg) 50% no-repeat;
    }
}
JavaScript
function lerp(A, B, t) {
    return A + t * (B - A);
}

function setProperty(element, dx, dy) { 
    element.style.setProperty(`--x`, `${dx}%`);
    element.style.setProperty(`--y`, `${dy}%`);  
}

Demo 地址:https://codepen.io/airen/full/poGGXBZ

线性插值应用得当,还可以构建出更复杂的动画效果。例如下面这个由 @Caleb Miller 提供的“布鲁姆”效果

Demo 地址:https://codepen.io/MillerTime/full/OJPBGyv (来源于 @Caleb Miller

详细代码不在这里展示了,感兴趣的同学请查看案例源代码。

我们还可以在一些交互中应用线性插值计算,例如应用线性插值可以使你的滚动效果看起来更平滑流畅:

JavaScript
const page = document.querySelector(".page");
const wrapper = document.querySelector(".page__wrapper");

const ease = 0.05;

const scroll = {
    current: 0,
    target: 0, 
    limit: 0
};

const updateTarget = (e) => {
    scroll.target += e.deltaY;
};

document.addEventListener("mousewheel", updateTarget);

// 线性插值计算
const lerp = (current, target) => {
    const distanceBetween = target - current;
    const distanceToTravel = distanceBetween * ease;
    return current + distanceToTravel;
};

const clamp = (min, max, value) => {
    const clamped = Math.min(Math.max(value, min), max);
    return clamped;
};

const smoothScroll = () => {
    const maxScroll = wrapper.clientHeight - window.innerHeight;
    scroll.target = clamp(0, maxScroll, scroll.target);

    const { current, target } = scroll;

    const transition = lerp(current, target);
    scroll.current = transition;

    wrapper.style.transform = `translateY(-${scroll.current}px)`;
    window.requestAnimationFrame(smoothScroll);
};

smoothScroll();

Demo 地址:https://codepen.io/airen/full/XWOOvdv

正如你所看到的,动画中的任意一属性两状态之间值变化,都可以应用线性插值来计算。

动画中的三角函数

现代 CSS 中引入了一些令人惊奇的新特性,其中就包括三角函数。它们解锁了许多以前不可能完成的任务。比如,定位元素位置、绘制不同的几何图形、创意布局和协调动画动作等。在这里,我们的核心是三角函数能给我们的动画创作带来哪些不一样的变化。

在解锁三角函数在动画中的应用之前,先简单地回顾一下三角函数。

CSS 的三角函数主要包括 sin()cos()tan()asin()acos()atan()atan2() 。其中 sin()cos()tan() 使用频率较大:

  • 正弦函数(sin(θ)) :定义为该角(θ)的对边(Opposite)与斜边(Hypotenuse)的比例,即 sin(θ) = Opposite / Hypotenuse = 对边 / 斜边 = a / c ,会返回一个角(θ)的正弦,它是一个介于 -11 之间的值。
  • 余弦函数(cos(θ)) :定义为该角(θ)的邻边(Adjacent)与斜边(Hypotenuse)的比例,即 cos(θ) = Adjacent / Hypotenuse = 邻边 / 斜边 = b ``/ c ,会返回一个角(θ)的余弦,它是一个介于 -11 之间的值。
  • 正切函数(tan(θ)) :定义为该角(θ)的对边(Opposite)与邻边(Adjacent)的比例,即 tan(θ) = Opposite / Adjacent = 对边 / 邻边 = a / b ,会返回一个角(θ)的正切,它是一个介于 -∞+∞ 之间的值。

正弦、余弦和正切等函数可以使用以下公式来描述:

code
sin(θ) = Opposite ÷ Hypotenuse = 对边 ÷ 斜边 = BC ÷ AB = a ÷ c
cos(θ) = Adjacent ÷ Hypotenuse = 邻边 ÷ 斜边 = AC ÷ AB = b ÷ c
tan(θ) = Opposite ÷ Adjacent = 对边 ÷ 邻边 = BC ÷ AC = a ÷ b = sin(θ) ÷ cos(θ)

通常会用希腊字母 θ 表示角度。

反三角函数( asin()acos()atan())是和三角函数(sin()cos()tan())有相对应关系的函数。

三角函数(sin()cos()tan())是用来计算一个给定角度的正弦、余弦和正切值的函数。例如,sin(θ) 表示角度 θ 的正弦值,cos(θ) 表示角度 θ 的余弦值,tan(θ) 表示角度 θ 的正切值。

而反三角函数(asin()acos()atan())则是用来计算给定正弦、余弦和正切值的角度的函数。例如,asin(x) 表示正弦值为 x 的角度,acos(x) 表示余弦值为 x 的角度,atan(x) 表示正切值为x 的角度。

这些反三角函数可以帮助我们在已知三角函数值时求出对应的角度,或者在已知角度时求出对应的三角函数值。它们成为三角函数与角度之间的桥梁,让我们可以在不同形式的表达之间转换。

我们在 CSS 中可以这样来使用 CSS 的三角函数,例如:

  • sin() 函数可用于改变元素尺寸或控制动画时长;
  • cos() 函数可用于保持旋转元素的尺寸不变;
  • tan() 函数可用于绘制平行四边形;
  • asin()acos()atan()atan2() 函数可用于旋转元素。

除此之外,三角函数在动画中有多种应用和作用,以下是一些常见的例子:

  • 平滑运动和缓动效果: 通过使用三角函数,特别是正弦(sin())和余弦(cos())函数,可以实现元素的平滑运动和缓动效果。这有助于使动画看起来更自然,避免了突然的加速和减速。
  • 周期性动画: 正弦(sin())和余弦(cos())函数是周期性的,因此它们经常用于创建具有循环或重复动作的动画效果。例如,通过调整正弦(sin())函数的参数,可以创建水波纹或震荡效果。
  • 旋转动画: 三角函数的旋转性质使其成为创建对象旋转动画的理想选择。通过在正弦(sin())和余弦(cos())函数中使用时间变量,可以实现平滑的旋转效果。
  • 路径动画: 三角函数的周期性和平滑性使其适用于定义对象沿着复杂路径运动的动画。通过适当调整三角函数的参数,可以创建各种路径动画。
  • 振荡效果: 通过使用正弦(sin())函数,可以模拟振荡效果,例如摆动或弹簧的弹性动画。
  • 相位和频率调整: 调整三角函数的相位和频率可以改变动画的速度和周期,从而提供更多的创造性控制。

这些只是三角函数在动画中的一些常见应用,创意的设计师和开发者可以发挥出更多的潜力,创造出丰富多彩且引人入胜的动画效果。

接下来,我们通过一些具体的案例来解锁三角函数在动画的应用。

解锁摩天轮动画

游乐场的摩天轮应该大家都玩过。接下来,我们就以这个为例,探讨一下如何使用 CSS 来完成一个类似摩天轮的动画效果。

就这个效果而言,有很多地方会用到三角函数,比如中间的不规则图形和摩天轮上每个吊蓝的位置。以摩天轮的吊蓝位置为例,我们可以通过三角函数来计算出它们的位置。

摩天轮有八个吊蓝,它们分别均匀的分布在一个圆环上。假设每个吊蓝是一个点,那么就会有八个点在这个圆环上。要是将这八个点相连,它们刚好可以构建出一个正八边形,正八边形的每个顶点就是吊蓝的位置。

正多边形每条边所对应的角(相邻两个顶点,也就是吊蓝的夹角)为 θ ,它刚好等于 360 度除以正多边形边数:

code
θ = 360 ÷ 正多边形边数

我们这个示例是一个正八边形,所以 θ 等于 45degθ = 360 ÷ 8 = 45)。有了这些参数之后,我们就可以使用三角函数计算出正八边形每个顶点(吊蓝)的位置,即它的 xy 坐标:

code
x = r * cos(θ)
y = r * sin(θ)

然后,我们可以逐个重复此过程,每次增加 θ 度的角度。使用这些公式,我们可以绘制任意数量边的顶点坐标。

我们来看具体的代码。构建这个上图摩天轮动画效果,我们需要像下面这样的一个 HTML 结构:

HTML
<div class="wheel">
    <!-- 摩天轮上半部分:圆环 + 吊篮 -->
    <div class="wheel__turntable">
        <!-- 摩天轮的圆环 -->
        <div class="wheel__circle"></div>
        
        <!-- 摩天轮圆环中心内装饰: 小圆 + 五角星 -->
        <div class="wheel__center"></div>
        
        <!-- 摩天轮圆环上四条线,挂吊篮的支持 -->
        <div class="wheel__line" style="--rotate: 0deg;"></div>
        <div class="wheel__line" style="--rotate: 90deg;"></div>
        
        <!-- 摩天轮的三个吊篮 -->
        <div class="basket" style="--index: 1;">
            <div class="basket__top"></div>
            <div class="basket__middle"></div>
            <div class="basket__bottom"></div>
        </div>
        <!-- 省略其他六个 basket -->
        <div class="basket" style="--index: 8;">
            <div class="basket__top"></div>
            <div class="basket__middle"></div>
            <div class="basket__bottom"></div>
        </div>
    </div>
    
    <!-- 摩天轮下半部分: 底座 -->
    <div class="wheel__base">
        <span></span>
    </div>
</div>

使用 CSS 来绘制摩天轮的外形,目前所有吊篮还位于摩天轮圆环的中心位置,同时也没有添加任何动画:

注意,我把示例中的窗花删除了,这主要是为了节约绘制摩天轮外形的时间,毕竟我们今天的主题是三角函数在动画中的应用,而不是如何绘制图形。要是大家喜欢的话,你可以使用 CSS 的 clip-path 和三角函数结合起来维制它。我在这里提供 @Michelle Barker 在 Codepen 上使用 clip-path 和三角函数制作了一个纸雪花生成器,你可以拖动控制手柄来剪切三角形的部分以生成雪花图案。clip-path 的坐标是使用三角函数计算的:

纸雪花生成器:https://codepen.io/michellebarker/full/VwKdONM (来源于 @Michelle Barker

正如你所看到的,在使用 CSS 绘制摩天轮外形时,也应用到了相关的几何知识,比如绘制圆、五角形、吊篮等。不过,我们更关注的是摩天轮每个吊篮的位置和摩天轮旋转动画效果。接下来,我将给出它们的答案。

首先能确定的是,摩天轮上有八个吊篮,我们可以用一个自定义属性来描述摩天轮上吊篮的个数,例 --count: 8 ,在这个基础上,我们就可以得出 θ 的值:

code
θ = 360deg ÷ var(--count)

同样,可以用一个自定义属性来描述 θ ,例如 --deg

CSS
:root {
    --count: 8; /* 摩天轮吊篮的数量 */
    --deg: calc(360deg / var(--count)); /* 相邻两吊篮之间夹角 */
}

到目前为止,所有摩天轮的所有吊篮都位于圆环的中心位置。假设,暂不考虑其他因素,按照常规方式,使用 translate 将所有吊篮从圆环中心位置沿水平方向移动到圆环上:

CSS
.basket {
    position: absolute;
    top: calc(50% - var(--radius-s));
    translate: calc(var(--radius) + var(--space) + var(--radius-s));
}

这样给每个吊篮定位是不够的,也是比较麻烦的,我们需要将 CSS 的三角函数应用进来,通过正弦(sin())和余弦(cos())函数来计算出它们的位置:

Demo 地址:https://codepen.io/airen/full/QWYovmb

如上所示,通过下面公式计算出每个吊篮的 xy 坐标,即 translatexy 偏移量:

CSS
translateX = cos(θ) × Radius = cos(var(--deg)) × Radius
translateY = sin(θ) × Radius = sin(var(--deg)) × Radius

在这个示例中,Radius 是圆环的半径 --radius 加上边框 --border 和外延出去的距离 --space 。同样的,我们用 --r 自定义属性来表示:

CSS
:root {
    --r: calc(var(--radius) + var(--border) + var(--space));
}

如此一来,角度 --deg 和斜边 --r 的值都有了:

CSS
:root {
    --count: 8;
    --deg: calc(360deg / var(--count));
    --r: calc(var(--radius) + var(--border) + var(--space));
}

将这些参数应用到 translatexy 上:

CSS
:root {
    --count: 8;
    --deg: calc(360deg / var(--count));
    --r: calc(var(--radius) + var(--border) + var(--space));
    
    --x: calc(cos(var(--deg)) * var(--r));
    --y: calc(sin(var(--deg)) * var(--r));
}

将用于 .baskettranslate 值用上面计算出来的 --x--y 替代:

CSS
.basket {
    position: absolute;
    top: calc(50% - var(--radius-s));
    translate: var(--x) var(--y);
}

此时,你将看到效果如下,吊篮的位置是根据三角函数计算出来的:

离我们的目标又更近一步了。接下来只需要根据 --index (它对应着摩天轮每个吊篮的索引值)来调整每个吊篮所对应的 --deg 值。

CSS
:root {
    --count: 8;  
    --r: calc(var(--radius) + var(--border) + var(--space));
}

.basket {
    --deg: calc(360deg / var(--count) * var(--index));
    --x: calc(cos(var(--deg)) * var(--r));
    --y: calc(sin(var(--deg)) * var(--r));
    
    position: absolute;
    top: calc(50% - var(--radius-s));
    translate: var(--x) var(--y);
}

是不是很简单。仅仅几行代码就实现了摩天轮的布局。

接下来就是给摩天轮添加旋转动画,使用 @keyframes 创作一个旋转动画很简单:

CSS
@keyframes rotation {
    to {
        rotate: -360deg;
    }
}

.wheel__turntable {
    animation: rotation 10s linear infinite;
}

不难发现,摩天轮是旋转了,但其中每个吊篮也跟着旋转了,这并不是我们所期望的摩天轮旋转。这是因为,我们直接旋转了摩天轮,造成其子元素(吊篮)也会旋转,为了避免这个现象,我们需要将摩天轮的旋转动画分成两部分,其中吊篮的旋转刚好与圆环的旋转相反,并且旋转中心也需要调整:

我们可以在吊篮上应用同一个关键帧动画 rotation ,只需要将动画的播放方向(animation-direction)设置为反向播放(reverse):

CSS
@keyframes rotation {
    to {
      rotate: -360deg;
    }
}
  
.wheel__turntable {
    animation: rotation 10s linear infinite;
    
    .basket {
        transform-origin:  center var(--radius-s);
        animation: rotation 10s linear infinite reverse;
    }
}

Demo 地址:https://codepen.io/airen/full/JjxzWgW

核心代码如下:

CSS
@layer animation {
    @keyframes rotation {
        to {
            rotate: -360deg;
        }
    }

    .wheel__turntable {
        animation: rotation 10s linear infinite;
    
        :is(.basket, .wheel__center) {
            animation: rotation 10s linear infinite reverse;
        }
    }

    :root {
        --radius: 30vh;     /* 圆环半径 */
        --border: 1vh;      /* 圆环边框宽度 */
    
        --radius-s: 1vh;    /* 吊篮顶部圆点半径 */
        --space: 4vh;       /* 圆环上直线伸出圆环的距离 */
        --line-height: 1vh; /* 圆环上直线厚度 */
        --count: 8;         /* 吊篮个数 */
        --r: calc(var(--radius) + var(--border) + var(--space)); /* 三角形斜边长度 */
    }
    
    .basket {
        --deg: calc(360deg / var(--count) * var(--index));
        --x: calc(cos(var(--deg)) * var(--r));
        --y: calc(sin(var(--deg)) * var(--r));
        position: absolute;
        top: calc(50% - var(--radius-s));
        translate: var(--x) var(--y);
        transform-origin: center var(--radius-s);
    }
}

在摩天轮动画效果的基础上,只需要更换一些 UI 皮肤,你就可以创作适用于生产中的互动动效,例如下面这个拆福袋的效果:

你还可以使用同样的方式来制作圆形的导航菜单:

Demo 地址:https://codepen.io/airen/full/oNmVoLm

以及一些加载动画,例如下面这个动画效果,它的高度就是通过三角函数计算得出的:

CSS
@layer animation {
    @property --divs {
        syntax: "<number>";
        initial-value: 15;
        inherits: false;
    }

    @property --rotation {
        syntax: "<angle>";
        initial-value: 0deg;
        inherits: false;
    }

    @keyframes wave {
        from {
            --rotation: 0deg;
        }
        to {
            --rotation: 360deg;
        }
    }

    .container > div {
        animation: 2s linear infinite wave;
    }
}

@layer demo {
    .container {
        --algin: start;
        --max: 30;
        --size: 10px;
        --gap: 4px;
        --divs: 15;
        --length-ratio: 30vh;
        --length: calc(var(--length-ratio) * (var(--divs) / var(--max)));
        --offset: calc(360deg / var(--divs));
        
        grid-template: 1fr / repeat(var(--divs), var(--size));
        display: grid;
        gap: var(--gap);
        place-self: center;
        min-height: 50vh;
    
        & > div {
            inline-size: var(--size);
            border-radius: 999rem;
            background-color: oklch(0.9 0.06 calc(var(--rotation) / 12 * var(--index)));
            block-size: calc( sin(calc(var(--rotation) + (var(--index) * var(--offset)))) * var(--length) + var(--size) + var(--length));
            place-self: var(--align);
        }
    }
}

Demo 地址:https://codepen.io/airen/full/poGYder

在上例的基础上,要是改变动画元素的外形,并且将它放置在一个圆形上,我们就可以轻易实现另一种动画效果,比如下面这个水波荡漾的动画效果:

Demo 地址:https://codepen.io/airen/full/MWLxqez

所有动画元素分布在一个半径为 --radius 圆环上,每个动画元素的定位使用的与摩天轮动画效果一样的机制,即三角函数计算出 translatexy 的值。上面示例我们在动画延迟时间、颜色计算等方面都应用了一定的数学计算,具体的代码如下:

CSS
@layer animation {
    @keyframes hexagon {
        100% {
            rotate: 360deg;
        }
    }
    
    :root {
        --duration: 3s;
    }
    
    .hexagon {
        --delay: calc((var(--index) + 1) * var(--duration) * (-3) / var(--hexagons));

         & span {
            animation: hexagon var(--duration) linear var(--delay) infinite;
        }
    }
}

@layer demo {
    :root {
        --leafage-size: 20vh;
        --dot-size: 1.5vh;
        --ratio-g: 31;
        --radius: 30vh;
        --hexagons: 15;
    }
    
    .hexagon {
        --ration: calc(-360deg / var(--hexagons) * (var(--index) + 1));
        --x: calc(cos(var(--ration)) * var(--radius));
        --y: calc(sin(var(--ration)) * var(--radius));
    
        --color: calc((var(--index) + 1) * 360 / var(--hexagons));

        color: hsl(var(--color) 100% 50% / 1);
        width: var(--leafage-size);
        aspect-ratio: 2 / 1;
        border-radius: 100% 0 100% 0;
        translate: var(--x) var(--y);

        & span {
            width: inherit;
            aspect-ratio: inherit;
            border-radius: inherit;
            background: hsl(var(--color) 100% 50% /0.5);
            position: relative;

            &::before,
            &::after {
                width: var(--dot-size);
                aspect-ratio: 1;
                border-radius: 50%;
                background-color: currentColor;
                position: absolute;
                top: calc(50% - var(--dot-size) / 2);
                left: calc(50% - var(--dot-size) / 2);
            }
    
            &::before {
                translate: calc(var(--leafage-size) / 2) calc(var(--leafage-size) / -4);
            }
    
            &::after {
                translate: calc(var(--leafage-size) / -2) calc(var(--leafage-size) / 4);
            }
        }
    }
}

你甚至还可以基于摩天轮动画,结合 CSS 的 3D 变换,让动画视觉具有深度感。

Demo 地址:https://codepen.io/web-dot-dev/full/poGVqLO (来源于 web.dev

这样一来,我们就可以制作一个 3D 旋转的幻灯片:

Demo 地址:https://codepen.io/airen/full/xxMBaQV

核心代码如下:

CSS
@layer animation {
    @keyframes carousel3dRotate {
        to {
            rotate: y 360deg;
        }
    }

    .items {
        animation: carousel3dRotate calc(var(--total) * 3s) linear infinite;
    
        &:hover {
            animation-play-state: paused;
        }
    }
}

@layer demo {
    .carousel3d {
        perspective: 1000px;
        display: grid;
        place-items: center;
    
        > *:not(:empty) {
            transform-style: preserve-3d;
        }
    
        .items {
            --width: 50vmin;
            --spacing: 0vmin;
            
            display: grid;
            width: var(--width);
            aspect-ratio: 1/1;
    
            --angle: calc(360deg / var(--total));
            --C: calc(var(--angle) / 2);
            --B: calc(90deg - var(--C));
            --c: calc(var(--width) / 2);
            --b: calc(var(--c) * sin(var(--B)) / sin(var(--C)));
            --tz: calc(var(--b) + var(--spacing, 0));
    
            translate: 0 0 calc(var(--tz) * -1);
        }
        .item {
            grid-area: 1/1;
            transform: rotateY(calc(var(--angle) * var(--index))) translateZ(var(--tz));
        }
    }
}

前面所呈现的案例基本上都是使用三角函数计算动画元素位置的,但这并不能代表三角函数仅用于动画元素位置的计算,它可以用于各种属性的计算。例如下面这个示例,使用三角函数来计算动画和延迟时间。

CSS
@layer animation {
    @keyframes fire {
        0% {
            background-position: left 0 top 25px;
        }
        50% {
            background-position: left 130px top -35px;
        }
        100% {
            background-position: left 260px top 0;
        }
    }
    
    @keyframes jump {
        50% {
            translate: 0 -150%;
        }
    }

    .loader span {
        --speed: 3;
        --count: 7;
        --delay: calc(sin((var(--index) / var(--count)) * 45deg) * var(--speed) * -1s);
    
        animation: 
            jump calc(var(--speed) * 1s) var(--delay) infinite ease-in-out,
            fire 15s linear infinite;
    }
}

Demo 地址:https://codepen.io/airen/full/JjxzmLO

解锁 CSS 中的随机值

三角函数中的正弦(sin())和余弦(cos())函数除了上面所呈现的特性之外,它还有另外两个特性:

  • 有界性:正弦和余弦函数的结果始终介于 -1 ~ 1 之间。这意味着,无念经你传递多大或多小的值,结果始终是在这个范围内
  • 连续性:正弦或余弦函数的输入进行微小更改将导致输出发生微小更改。这一特性可以在动画过程中实现值的逐渐变化,同时系统仍然表现出随机性

先来看一下有界性。正弦和余弦函数的结果始终介于 -1 ~ 1 之间。这意味着,无论你传递多大或多小的值,结果始终是这个范围内的值。然后,我们可以对值进行简单的归一化到 [0,1] 范围。有了归一化的值,我们可以使用简单的线性映射来表示任何值。例如:

CSS
.element {
    --x: calc(0.5 + 0.5 * sin(var(--n) * 342.06184 + 23.434));

    background: rgb(calc(50 + var(--x) * 100), 0, 0);
}

上面代码中 rgb() 函数中的 r 值始终会在 50 ~ 100 范围内。我们使用正弦函数将其值映射到 [-1,1] 范围。最后,如下面的动画所示,我们可以通过应用简单的代数变换将其映射到 [0, 1] 范围。一旦我们从 [0,1] 范围内获取值,我们可以使用线性映射将其映射到任何其他所需的值。

正弦和余弦函数的另一特性是连续性,简单地说,你可以认为正弦或余弦函数的输入进行微小更改将导致输出发生微小更改。由于这一特性,我们可以在动画过程中实现值的逐渐变化,同时系统仍然表现出随机性。

也就是说,我们可以利用三角函数这些特性来生成伪随机值。例如下面这个示例,使用 CSS 的正弦函数生成随机的值。动画元素的颜色和宽度是随机产生的:

CSS
div::before {
    --x: calc(0.5 + 0.5 * sin(4.284 * var(--n)));
    --y: calc(0.5 + 0.5 * sin(7.284 * var(--n)));
    --z: calc(0.5 + 0.5 * sin(4 * var(--n) + 2 * var(--t)));
    --w: calc(0.5 + 0.5 * sin((0.2 * cos(var(--t)/100) + 0.8) * 49.123 * var(--n) + var(--t)));
  
    background: rgb(
        calc(50 +  100 * var(--x))  /* R 通道 */
        calc(200 + 30 * var(--y))   /* G 通道 */
        calc(120 + 100 * var(--z))  /* B 通道 */
    );
    width: calc(50% + var(--w)*50%); /* 元素宽度 */
}

最后两个变量,除了我们的计数器 --n,还使用了时间变量 --t,通过运行逐渐改变该变量的动画获得:

CSS
@property --t {
    syntax: '<number>'; 
    initial-value: 0;
    inherits: true;
}

:root {
    --t: 0;
}

@keyframes timeOn {
    50% {
        --t: 30
    }
}

html {
    animation: 30s timeOn infinite linear;
}

Demo 地址:https://codepen.io/hypersphere/full/QWJBOgG (来源于 @Hypersphere

这一特性还非常适合用来模拟一些物理效果。例如,使用它制作钟摆的动画效果:

CSS
@layer animation {
    @keyframes moveShade {
        0% {
            translate: -220px 0;
            scale: 1.4 0.5;
            filter: blur(20px);
        }
        50% {
            filter: blur(3px);
        }
        100% {
            translate: 220px 0;
            scale: 1.4 0.5;
            filter: blur(20px);
        }
    }
    
    .shadow {
        animation: moveShade 1.25s ease-in-out alternate infinite;
    }

    @keyframes move {
        from {
            --t: 0;
        }
        to {
            --t: 180;
        }
    }

    @property --t {
        syntax: "<number>";
        initial-value: 0;
        inherits: true;
    }

    .thread {
        --PI: 3.141592653589793;
        --r: calc(cos(var(--t) / 180 * var(--PI)) * var(--PI) * 0.25rad);
        transform-origin: 50% 0;
        rotate: z var(--r);
        animation: move 1.25s ease-in-out infinite alternate;
    }
}

Demo 地址:https://codepen.io/airen/full/JjxzeNe

总的来说,CSS 三角函数和动画为 Web 设计师和开发人员提供了创造性可能性的世界。通过在 CSS 中利用三角学的力量,我们可以为动画注入动态和引人入胜的元素,同时受益于 CSS 解决方案提供的性能优势和减少依赖的优势。

小结

在这节课中,向大家阐述了动画中的数学基础,并围绕着线性插值计算和三角函数展开了详细的介绍。从这两个方面我们可以领略到数学知识在动画制作中的重要性。拿三角函数来说,我们可以通过三角函数来绘制几何图形、给动画元素定位、调整动画元素旋转角度、设置动画延迟时间等等。

我们可以通过相关的数学知识和概念创造出更加逼真、流畅和有趣的动画效果。虽然课程只对部分数学知识在动画中应用展开了介绍,但大家要知道的是,数学不仅仅是一种工具,更是解锁创意和创新的关键。如果你在这方面的基础足够好,你将能创造出更多更优秀的动画。