cvs
Version:
A basic canvas framework for 2d drawing.
160 lines (151 loc) • 4.84 kB
JavaScript
import { error, forObj } from './utils/utils'
import { Easings, getBezierEasing } from './utils/easing'
import colorString from 'color-string'
/**
* 计算当前帧属性
* @f: startFrame
* @t: endFrame
* @p: progress
* @s: startOffset
* @e: endOffset
*/
function calculate (f, t, p, s, e) {
if (typeof f === 'number' && typeof t === 'number') {
return f + ((p - s) / (e - s)) * (t - f)
}
if (p - s > e - p) return t
return f
}
// 计算 offset
function calculateFramesOffset (keyframes) {
keyframes = keyframes.slice(0)
const firstFrame = keyframes[0],
lastFrame = keyframes[keyframes.length - 1]
// 第一帧和最后一帧的 offset 是固定的
lastFrame.offset = 1
firstFrame.offset = 0
let offset = 0,
offsetFrom = -1
for (let i = 0; i < keyframes.length; i++) {
const frame = keyframes[i]
if (frame.offset != null) {
const dis = i - offsetFrom
if (dis > 1) {
const delta = (frame.offset - offset) / dis
for (let j = 0; j < dis - 1; j++) {
keyframes[offsetFrom + j + 1].offset = offset + delta * (j + 1)
}
}
offset = frame.offset
offsetFrom = i
}
if (i > 0) {
// 如果中间某个属性没有了,需要从前一帧复制过来
// keyframes[i] = Object.assign({}, keyframes[i - 1], keyframes[i])
}
}
return keyframes
}
const _timing = Symbol('timing'),
_easing = Symbol('easing'),
_keyframes = Symbol('keyframes'),
_element = Symbol('element'),
_cb = Symbol('cb')
export default class Keyframe {
constructor (element, keyframes, timing, cb) {
if (keyframes.length < 2) {
error('keyframes need at least two items.')
}
if (Array.isArray(keyframes[0]) || typeof keyframes[0] === 'string') {
let temp = {}
if (typeof keyframes[0] === 'string') keyframes[0] = [keyframes[0]]
keyframes[0].forEach(item => {
if (item === 'offsetDistance') temp.offsetDistance = 0
else temp[item] = element.attr(item)
})
keyframes[0] = temp
}
if (keyframes[0].fill || keyframes[0].stroke) {
keyframes.forEach(item => {
if (item.fill) item.fill = colorString.get.rgb(item.fill)
if (item.stroke) item.stroke = colorString.get.rgb(item.stroke)
})
}
// 支持 duration 省略传参
if (typeof timing === 'number') timing = { duration: timing }
if (timing.duration <= 0) {
error('duration must a number that greater than 0.')
}
this[_timing] = Object.assign(
{},
{ iterations: 1, easing: 'linear', delay: 0 },
timing
)
const easingType = this[_timing].easing
if (typeof easingType === 'string') this[_easing] = Easings[easingType]
else if (Array.isArray(easingType) && easingType.length === 4) { this[_easing] = getBezierEasing(...easingType) } else error('easing must be a string or an array has four items')
this[_keyframes] = calculateFramesOffset(keyframes)
this[_element] = element
this[_cb] = cb
this.cbEmitCount = 0
}
currentFrame (p) {
let res = {}
let frames = this[_keyframes]
frames.some((item, index) => {
if (item.offset > p || item.offset === 1) {
res.f = index === 0 ? item : frames[index - 1]
res.t = item
return true
}
})
return res
}
result (p) {
let { f, t } = this.currentFrame(p)
let result = {}
forObj(f, (key, val) => {
if (/\b(offset)\b/.test(key)) return
if (/\b(fill|stroke)\b/.test(key)) {
result[key] = colorString.to.rgb([
calculate(val[0], t[key][0], p, f.offset, t.offset),
calculate(val[1], t[key][1], p, f.offset, t.offset),
calculate(val[2], t[key][2], p, f.offset, t.offset),
calculate(val[3], t[key][3], p, f.offset, t.offset)
])
} else {
result[key] = calculate(val, t[key], p, f.offset, t.offset)
}
})
return result
}
run (t) {
if (this.cbEmitCount === 2) return
let p = (t - this[_timing].delay) / this[_timing].duration
if (p >= 1 && this.cbEmitCount === 0) {
// 保证最后一帧是终点
this[_element].attr(this.handle(this.result(1)))
this.cbEmitCount = 1
return
} else if (p >= 1 && this.cbEmitCount === 1) {
this[_cb] && this[_cb].call(this[_element], this[_element])
this.cbEmitCount = 2
return
}
// this.cbEmitCount = 0
p = this[_easing](p)
this[_element].attr(this.handle(this.result(p)))
}
handle (obj) {
if (obj.offsetDistance != null) {
const len = this[_element].attr('offsetPath').getTotalLength()
const [x, y] = this[_element]
.attr('offsetPath')
.getPointAtLength(len * obj.offsetDistance)
obj.x = x
obj.y = y
delete obj.offsetDistance
}
return obj
}
}