cubic-bezier三次贝塞尔时间函数

time ratio output ratio

{{ cubicBezierStr }}

用在CSS的transition或者animation动画中的时间函数timing-function,描述在过渡或动画中一维数值的速度改变,通常称为缓动函数。

三次/立方贝塞尔曲线(cubic Bézier curves)是CSS时间函数常用的一种。语法为:

cubic-bezier(x1, y1, x2, y2)
  • 横轴为时间比例(time ratio),纵轴为完成状态(output ratio)

  • 曲线由四个点来定义,P0、P1、P2和P3

  • P0(0, 0)和P3(1, 1)是固定在坐标系上的起点和终点

  • 语法中x1, y1, x2, y2表示两个过渡点P1和P2的横纵坐标

  • x1和x2是时间值,必须在[0, 1]范围内

  • y1和y2可以是负数或者大于1,从而实现弹跳动画效果

  • y值超过实际允许范围(比如颜色值大于255或小于0)会被修改为允许范围内的最接近值

常用的命名过渡效果,等同于某些数值的cubic-bezier。

名称 过渡效果 等同于
linear 以相同速度开始至结束 cubic-bezier(0, 0, 1, 1)
ease 慢速开始,然后变快,然后慢速结束 cubic-bezier(.25, .1, .25, 1)
ease-in 慢速开始 cubic-bezier(.42, 0, 1, 1)
ease-out 慢速结束 cubic-bezier(0, 0, .58, 1)
ease-in-out 慢速开始和结束 cubic-bezier(.42, 0, .58, 1)

本页面用Vue实现响应式SVG来简单模拟cubic-bezier三次贝塞尔时间函数。

SVG画布和cubic-bezier过渡点的坐标系不同需要分别定义。

data: {
// SVG画布中的坐标
x1: 50,
y1: 180,
x2: 50,
y2: 0,

// cubic-bezier过渡点的坐标
cx1: 0.25,
cy1: 0.1,
cx2: 0.25,
cy2: 1,
}

坐标转换,将时间值约束在[0, 1]范围内。

computed: {
cubicBezierStr() {
const f = n => {
let r = String(n.toFixed(2))
r = r.replace(/^0+|0+$/g, '')
r = r.replace(/\.$/, '')
if (r === '') r = 0
return r
}
const { x1, y1, x2, y2 } = this
const cx1 = x1 / 200
const cy1 = (200 - y1) / 200
const cx2 = x2 / 200
const cy2 = (200 - y2) / 200
return `cubic-bezier(${f(cx1)}, ${f(cy1)}, ${f(cx2)}, ${f(cy2)})`
}
}

实现过渡点的拖动与计算,兼容桌面和移动设备mousemove/touchmove。

methods: {
handleStart(event, point) {
event.preventDefault()
const isTouch = !!event.touches
if (isTouch && event.touches.length > 1) return
if (isTouch) event = event.touches[0]

const { x1, y1, x2, y2 } = this
this.dragState = {
dragging: true,
left: event.clientX,
top: event.clientY
}

document.onselectstart = () => false
document.ondragstart = () => false

const handleMove = event => {
event.preventDefault()
if (isTouch) event = event.touches[0]

const constrain = n => Math.min(Math.max(0, n), 200)
const deltaLeft = event.clientX - this.dragState.left
const deltaTop = event.clientY - this.dragState.top
if (point === 1) {
this.x1 = constrain(x1 + deltaLeft)
this.y1 = y1 + deltaTop
} else if (point === 2) {
this.x2 = constrain(x2 + deltaLeft)
this.y2 = y2 + deltaTop
}
}

const handleEnd = () => {
this.dragState.dragging = false
if (isTouch) {
document.removeEventListener('touchmove', handleMove)
document.removeEventListener('touchend', handleEnd)
} else {
document.removeEventListener('mousemove', handleMove)
document.removeEventListener('mouseup', handleEnd)
}
document.onselectstart = null
document.ondragstart = null
}

if (isTouch) {
document.addEventListener('touchmove', handleMove, {
passive: false
})
document.addEventListener('touchend', handleEnd)
} else {
document.addEventListener('mousemove', handleMove)
document.addEventListener('mouseup', handleEnd)
}
}
}