apexcharts
Version:
A JavaScript Chart Library
269 lines (242 loc) • 6.7 kB
JavaScript
// @ts-check
import { morphPaths } from './PathMorphing'
import { BrowserAPIs } from '../ssr/BrowserAPIs.js'
// Sine ease in-out (matches SVG.js default '<>' easing)
/**
* @param {number} t
*/
function easeInOut(t) {
return -Math.cos(t * Math.PI) / 2 + 0.5
}
// Parse color string to [r, g, b, a]
/**
* @param {string} str
*/
function parseColor(str) {
if (!str || typeof str !== 'string') return null
// hex #rgb or #rrggbb
if (str[0] === '#') {
let hex = str.slice(1)
if (hex.length === 3)
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
const n = parseInt(hex, 16)
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1]
}
// rgb(r,g,b) or rgba(r,g,b,a)
const m = str.match(
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/,
)
if (m) return [+m[1], +m[2], +m[3], m[4] !== undefined ? +m[4] : 1]
return null
}
/**
* @param {number[]} from
* @param {number[]} to
* @param {number} pos
*/
function interpolateColor(from, to, pos) {
return `rgba(${Math.round(from[0] + (to[0] - from[0]) * pos)},${Math.round(from[1] + (to[1] - from[1]) * pos)},${Math.round(from[2] + (to[2] - from[2]) * pos)},${from[3] + (to[3] - from[3]) * pos})`
}
class SVGAnimationRunner {
/**
* @param {any} element
* @param {number} duration
* @param {number} delay
*/
constructor(element, duration, delay) {
this.el = element
this.duration = duration ?? 300
this.delay = delay || 0
this._attrTarget = null
this._plotTarget = null
this._afterCb = null
this._duringCb = null
this._next = null
/** @type {SVGAnimationRunner | null} */
this._root = null
this._scheduled = false
}
/**
* @param {Record<string, any>} to
*/
attr(to) {
this._attrTarget = to
this._schedule()
return this
}
/**
* @param {string} d
*/
plot(d) {
this._plotTarget = d
this._schedule()
return this
}
/**
* @param {Function} fn
*/
after(fn) {
this._afterCb = fn
this._schedule()
return this
}
/**
* @param {Function} fn
*/
during(fn) {
this._duringCb = fn
this._schedule()
return this
}
/**
* @param {number} duration
* @param {number} delay
*/
animate(duration, delay) {
const next = new SVGAnimationRunner(this.el, duration, delay)
this._next = next
next._root = this._root || this
return next
}
_schedule() {
const root = this._root || this
if (!root._scheduled) {
root._scheduled = true
queueMicrotask(() => root._executeChain())
}
}
_executeChain() {
const chain = []
/** @type {any} */ let r = this
while (r) {
chain.push(r)
r = r._next
}
let cumulativeDelay = 0
chain.forEach((runner) => {
cumulativeDelay += runner.delay
runner._execute(cumulativeDelay)
cumulativeDelay += runner.duration
})
}
/**
* @param {number} startDelay
*/
_execute(startDelay) {
const el = this.el
const duration = this.duration
if (duration <= 1) {
// Near-instant: just apply final state after delay
const apply = () => {
if (this._attrTarget) el.attr(this._attrTarget)
if (this._plotTarget) el.plot(this._plotTarget)
if (this._afterCb) this._afterCb.call(el)
}
if (startDelay > 0) {
setTimeout(apply, startDelay)
} else {
apply()
}
return
}
const run = () => {
// Capture "from" values for attr interpolation
const fromAttrs = /** @type {Record<string, any>} */ ({})
const fromColors = /** @type {Record<string, any>} */ ({})
const toColors = /** @type {Record<string, any>} */ ({})
if (this._attrTarget) {
for (const key of Object.keys(this._attrTarget)) {
const fromVal = el.attr(key)
fromAttrs[key] = fromVal
// Check if this is a color value
const fc = parseColor(fromVal)
const tc = parseColor(String(this._attrTarget[key]))
if (fc && tc) {
fromColors[key] = fc
toColors[key] = tc
}
}
}
// Initialize path morpher if needed
/** @type {Function | null} */
let morphFn = null
if (this._plotTarget) {
const fromPath = el.attr('d') || ''
try {
morphFn = morphPaths(fromPath, this._plotTarget)
} catch (e) {
// If path morphing fails, just snap at the end
morphFn = null
}
}
const start = performance.now()
/**
* @param {number} now
*/
const tick = (now) => {
const elapsed = now - start
const rawPos = Math.min(elapsed / duration, 1)
const pos = easeInOut(rawPos)
// Interpolate attributes
if (this._attrTarget) {
if (rawPos >= 1) {
el.attr(this._attrTarget)
} else {
const current = /** @type {Record<string, any>} */ ({})
for (const key of Object.keys(this._attrTarget)) {
if (fromColors[key] && toColors[key]) {
current[key] = interpolateColor(
fromColors[key],
toColors[key],
pos,
)
} else {
const from = parseFloat(fromAttrs[key])
const to = parseFloat(this._attrTarget[key])
if (!isNaN(from) && !isNaN(to)) {
current[key] = from + (to - from) * pos
}
}
}
el.attr(current)
}
}
// Path morphing
if (morphFn && rawPos < 1) {
el.attr('d', /** @type {any} */ (morphFn)(pos))
}
// During callback (pass raw linear position, matching SVG.js behavior)
if (this._duringCb) this._duringCb(pos)
if (rawPos < 1) {
BrowserAPIs.requestAnimationFrame(tick)
} else {
// Set final path
if (this._plotTarget) {
el.attr('d', this._plotTarget)
}
if (this._afterCb) this._afterCb.call(el)
}
}
BrowserAPIs.requestAnimationFrame(tick)
}
if (startDelay > 0) {
setTimeout(run, startDelay)
} else {
run()
}
}
}
// Install .animate() on SVGElement prototype
/**
* @param {any} ElementClass
*/
function installAnimationMethods(ElementClass) {
/**
* @param {number} duration
* @param {number} delay
*/
ElementClass.prototype.animate = function (duration, delay) {
return new SVGAnimationRunner(this, duration, delay)
}
}
export { SVGAnimationRunner, installAnimationMethods }