Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
import { globals } from '../utils/window.js'
import Queue from './Queue.js'
const Animator = {
nextDraw: null,
frames: new Queue(),
timeouts: new Queue(),
immediates: new Queue(),
timer: () => globals.window.performance || globals.window.Date,
transforms: [],
frame(fn) {
// Store the node
const node = Animator.frames.push({ run: fn })
// Request an animation frame if we don't have one
if (Animator.nextDraw === null) {
Animator.nextDraw = globals.window.requestAnimationFrame(Animator._draw)
}
// Return the node so we can remove it easily
return node
},
timeout(fn, delay) {
delay = delay || 0
// Work out when the event should fire
const time = Animator.timer().now() + delay
// Add the timeout to the end of the queue
const node = Animator.timeouts.push({ run: fn, time: time })
// Request another animation frame if we need one
if (Animator.nextDraw === null) {
Animator.nextDraw = globals.window.requestAnimationFrame(Animator._draw)
}
return node
},
immediate(fn) {
// Add the immediate fn to the end of the queue
const node = Animator.immediates.push(fn)
// Request another animation frame if we need one
if (Animator.nextDraw === null) {
Animator.nextDraw = globals.window.requestAnimationFrame(Animator._draw)
}
return node
},
cancelFrame(node) {
node != null && Animator.frames.remove(node)
},
clearTimeout(node) {
node != null && Animator.timeouts.remove(node)
},
cancelImmediate(node) {
node != null && Animator.immediates.remove(node)
},
_draw(now) {
// Run all the timeouts we can run, if they are not ready yet, add them
// to the end of the queue immediately! (bad timeouts!!! [sarcasm])
let nextTimeout = null
const lastTimeout = Animator.timeouts.last()
while ((nextTimeout = Animator.timeouts.shift())) {
// Run the timeout if its time, or push it to the end
if (now >= nextTimeout.time) {
nextTimeout.run()
} else {
Animator.timeouts.push(nextTimeout)
}
// If we hit the last item, we should stop shifting out more items
if (nextTimeout === lastTimeout) break
}
// Run all of the animation frames
let nextFrame = null
const lastFrame = Animator.frames.last()
while (nextFrame !== lastFrame && (nextFrame = Animator.frames.shift())) {
nextFrame.run(now)
}
let nextImmediate = null
while ((nextImmediate = Animator.immediates.shift())) {
nextImmediate()
}
// If we have remaining timeouts or frames, draw until we don't anymore
Animator.nextDraw =
Animator.timeouts.first() || Animator.frames.first()
? globals.window.requestAnimationFrame(Animator._draw)
: null
}
}
export default Animator

View File

@@ -0,0 +1,231 @@
import { timeline } from '../modules/core/defaults.js'
import { extend } from '../utils/adopter.js'
/***
Base Class
==========
The base stepper class that will be
***/
function makeSetterGetter(k, f) {
return function (v) {
if (v == null) return this[k]
this[k] = v
if (f) f.call(this)
return this
}
}
export const easing = {
'-': function (pos) {
return pos
},
'<>': function (pos) {
return -Math.cos(pos * Math.PI) / 2 + 0.5
},
'>': function (pos) {
return Math.sin((pos * Math.PI) / 2)
},
'<': function (pos) {
return -Math.cos((pos * Math.PI) / 2) + 1
},
bezier: function (x1, y1, x2, y2) {
// see https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
return function (t) {
if (t < 0) {
if (x1 > 0) {
return (y1 / x1) * t
} else if (x2 > 0) {
return (y2 / x2) * t
} else {
return 0
}
} else if (t > 1) {
if (x2 < 1) {
return ((1 - y2) / (1 - x2)) * t + (y2 - x2) / (1 - x2)
} else if (x1 < 1) {
return ((1 - y1) / (1 - x1)) * t + (y1 - x1) / (1 - x1)
} else {
return 1
}
} else {
return 3 * t * (1 - t) ** 2 * y1 + 3 * t ** 2 * (1 - t) * y2 + t ** 3
}
}
},
// see https://www.w3.org/TR/css-easing-1/#step-timing-function-algo
steps: function (steps, stepPosition = 'end') {
// deal with "jump-" prefix
stepPosition = stepPosition.split('-').reverse()[0]
let jumps = steps
if (stepPosition === 'none') {
--jumps
} else if (stepPosition === 'both') {
++jumps
}
// The beforeFlag is essentially useless
return (t, beforeFlag = false) => {
// Step is called currentStep in referenced url
let step = Math.floor(t * steps)
const jumping = (t * step) % 1 === 0
if (stepPosition === 'start' || stepPosition === 'both') {
++step
}
if (beforeFlag && jumping) {
--step
}
if (t >= 0 && step < 0) {
step = 0
}
if (t <= 1 && step > jumps) {
step = jumps
}
return step / jumps
}
}
}
export class Stepper {
done() {
return false
}
}
/***
Easing Functions
================
***/
export class Ease extends Stepper {
constructor(fn = timeline.ease) {
super()
this.ease = easing[fn] || fn
}
step(from, to, pos) {
if (typeof from !== 'number') {
return pos < 1 ? from : to
}
return from + (to - from) * this.ease(pos)
}
}
/***
Controller Types
================
***/
export class Controller extends Stepper {
constructor(fn) {
super()
this.stepper = fn
}
done(c) {
return c.done
}
step(current, target, dt, c) {
return this.stepper(current, target, dt, c)
}
}
function recalculate() {
// Apply the default parameters
const duration = (this._duration || 500) / 1000
const overshoot = this._overshoot || 0
// Calculate the PID natural response
const eps = 1e-10
const pi = Math.PI
const os = Math.log(overshoot / 100 + eps)
const zeta = -os / Math.sqrt(pi * pi + os * os)
const wn = 3.9 / (zeta * duration)
// Calculate the Spring values
this.d = 2 * zeta * wn
this.k = wn * wn
}
export class Spring extends Controller {
constructor(duration = 500, overshoot = 0) {
super()
this.duration(duration).overshoot(overshoot)
}
step(current, target, dt, c) {
if (typeof current === 'string') return current
c.done = dt === Infinity
if (dt === Infinity) return target
if (dt === 0) return current
if (dt > 100) dt = 16
dt /= 1000
// Get the previous velocity
const velocity = c.velocity || 0
// Apply the control to get the new position and store it
const acceleration = -this.d * velocity - this.k * (current - target)
const newPosition = current + velocity * dt + (acceleration * dt * dt) / 2
// Store the velocity
c.velocity = velocity + acceleration * dt
// Figure out if we have converged, and if so, pass the value
c.done = Math.abs(target - newPosition) + Math.abs(velocity) < 0.002
return c.done ? target : newPosition
}
}
extend(Spring, {
duration: makeSetterGetter('_duration', recalculate),
overshoot: makeSetterGetter('_overshoot', recalculate)
})
export class PID extends Controller {
constructor(p = 0.1, i = 0.01, d = 0, windup = 1000) {
super()
this.p(p).i(i).d(d).windup(windup)
}
step(current, target, dt, c) {
if (typeof current === 'string') return current
c.done = dt === Infinity
if (dt === Infinity) return target
if (dt === 0) return current
const p = target - current
let i = (c.integral || 0) + p * dt
const d = (p - (c.error || 0)) / dt
const windup = this._windup
// antiwindup
if (windup !== false) {
i = Math.max(-windup, Math.min(i, windup))
}
c.error = p
c.integral = i
c.done = Math.abs(p) < 0.001
return c.done ? target : current + (this.P * p + this.I * i + this.D * d)
}
}
extend(PID, {
windup: makeSetterGetter('_windup'),
p: makeSetterGetter('P'),
i: makeSetterGetter('I'),
d: makeSetterGetter('D')
})

View File

@@ -0,0 +1,336 @@
import { Ease } from './Controller.js'
import {
delimiter,
numberAndUnit,
isPathLetter
} from '../modules/core/regex.js'
import { extend } from '../utils/adopter.js'
import Color from '../types/Color.js'
import PathArray from '../types/PathArray.js'
import SVGArray from '../types/SVGArray.js'
import SVGNumber from '../types/SVGNumber.js'
const getClassForType = (value) => {
const type = typeof value
if (type === 'number') {
return SVGNumber
} else if (type === 'string') {
if (Color.isColor(value)) {
return Color
} else if (delimiter.test(value)) {
return isPathLetter.test(value) ? PathArray : SVGArray
} else if (numberAndUnit.test(value)) {
return SVGNumber
} else {
return NonMorphable
}
} else if (morphableTypes.indexOf(value.constructor) > -1) {
return value.constructor
} else if (Array.isArray(value)) {
return SVGArray
} else if (type === 'object') {
return ObjectBag
} else {
return NonMorphable
}
}
export default class Morphable {
constructor(stepper) {
this._stepper = stepper || new Ease('-')
this._from = null
this._to = null
this._type = null
this._context = null
this._morphObj = null
}
at(pos) {
return this._morphObj.morph(
this._from,
this._to,
pos,
this._stepper,
this._context
)
}
done() {
const complete = this._context.map(this._stepper.done).reduce(function (
last,
curr
) {
return last && curr
}, true)
return complete
}
from(val) {
if (val == null) {
return this._from
}
this._from = this._set(val)
return this
}
stepper(stepper) {
if (stepper == null) return this._stepper
this._stepper = stepper
return this
}
to(val) {
if (val == null) {
return this._to
}
this._to = this._set(val)
return this
}
type(type) {
// getter
if (type == null) {
return this._type
}
// setter
this._type = type
return this
}
_set(value) {
if (!this._type) {
this.type(getClassForType(value))
}
let result = new this._type(value)
if (this._type === Color) {
result = this._to
? result[this._to[4]]()
: this._from
? result[this._from[4]]()
: result
}
if (this._type === ObjectBag) {
result = this._to
? result.align(this._to)
: this._from
? result.align(this._from)
: result
}
result = result.toConsumable()
this._morphObj = this._morphObj || new this._type()
this._context =
this._context ||
Array.apply(null, Array(result.length))
.map(Object)
.map(function (o) {
o.done = true
return o
})
return result
}
}
export class NonMorphable {
constructor(...args) {
this.init(...args)
}
init(val) {
val = Array.isArray(val) ? val[0] : val
this.value = val
return this
}
toArray() {
return [this.value]
}
valueOf() {
return this.value
}
}
export class TransformBag {
constructor(...args) {
this.init(...args)
}
init(obj) {
if (Array.isArray(obj)) {
obj = {
scaleX: obj[0],
scaleY: obj[1],
shear: obj[2],
rotate: obj[3],
translateX: obj[4],
translateY: obj[5],
originX: obj[6],
originY: obj[7]
}
}
Object.assign(this, TransformBag.defaults, obj)
return this
}
toArray() {
const v = this
return [
v.scaleX,
v.scaleY,
v.shear,
v.rotate,
v.translateX,
v.translateY,
v.originX,
v.originY
]
}
}
TransformBag.defaults = {
scaleX: 1,
scaleY: 1,
shear: 0,
rotate: 0,
translateX: 0,
translateY: 0,
originX: 0,
originY: 0
}
const sortByKey = (a, b) => {
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0
}
export class ObjectBag {
constructor(...args) {
this.init(...args)
}
align(other) {
const values = this.values
for (let i = 0, il = values.length; i < il; ++i) {
// If the type is the same we only need to check if the color is in the correct format
if (values[i + 1] === other[i + 1]) {
if (values[i + 1] === Color && other[i + 7] !== values[i + 7]) {
const space = other[i + 7]
const color = new Color(this.values.splice(i + 3, 5))
[space]()
.toArray()
this.values.splice(i + 3, 0, ...color)
}
i += values[i + 2] + 2
continue
}
if (!other[i + 1]) {
return this
}
// The types differ, so we overwrite the new type with the old one
// And initialize it with the types default (e.g. black for color or 0 for number)
const defaultObject = new other[i + 1]().toArray()
// Than we fix the values array
const toDelete = values[i + 2] + 3
values.splice(
i,
toDelete,
other[i],
other[i + 1],
other[i + 2],
...defaultObject
)
i += values[i + 2] + 2
}
return this
}
init(objOrArr) {
this.values = []
if (Array.isArray(objOrArr)) {
this.values = objOrArr.slice()
return
}
objOrArr = objOrArr || {}
const entries = []
for (const i in objOrArr) {
const Type = getClassForType(objOrArr[i])
const val = new Type(objOrArr[i]).toArray()
entries.push([i, Type, val.length, ...val])
}
entries.sort(sortByKey)
this.values = entries.reduce((last, curr) => last.concat(curr), [])
return this
}
toArray() {
return this.values
}
valueOf() {
const obj = {}
const arr = this.values
// for (var i = 0, len = arr.length; i < len; i += 2) {
while (arr.length) {
const key = arr.shift()
const Type = arr.shift()
const num = arr.shift()
const values = arr.splice(0, num)
obj[key] = new Type(values) // .valueOf()
}
return obj
}
}
const morphableTypes = [NonMorphable, TransformBag, ObjectBag]
export function registerMorphableType(type = []) {
morphableTypes.push(...[].concat(type))
}
export function makeMorphable() {
extend(morphableTypes, {
to(val) {
return new Morphable()
.type(this.constructor)
.from(this.toArray()) // this.valueOf())
.to(val)
},
fromArray(arr) {
this.init(arr)
return this
},
toConsumable() {
return this.toArray()
},
morph(from, to, pos, stepper, context) {
const mapper = function (i, index) {
return stepper.step(i, to[index], pos, context[index], context)
}
return this.fromArray(from.map(mapper))
}
})
}

View File

@@ -0,0 +1,62 @@
export default class Queue {
constructor() {
this._first = null
this._last = null
}
// Shows us the first item in the list
first() {
return this._first && this._first.value
}
// Shows us the last item in the list
last() {
return this._last && this._last.value
}
push(value) {
// An item stores an id and the provided value
const item =
typeof value.next !== 'undefined'
? value
: { value: value, next: null, prev: null }
// Deal with the queue being empty or populated
if (this._last) {
item.prev = this._last
this._last.next = item
this._last = item
} else {
this._last = item
this._first = item
}
// Return the current item
return item
}
// Removes the item that was returned from the push
remove(item) {
// Relink the previous item
if (item.prev) item.prev.next = item.next
if (item.next) item.next.prev = item.prev
if (item === this._last) this._last = item.prev
if (item === this._first) this._first = item.next
// Invalidate item
item.prev = null
item.next = null
}
shift() {
// Check if we have a value
const remove = this._first
if (!remove) return null
// If we do, remove it and relink things
this._first = remove.next
if (this._first) this._first.prev = null
this._last = this._first ? this._last : null
return remove.value
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
import { globals } from '../utils/window.js'
import { registerMethods } from '../utils/methods.js'
import Animator from './Animator.js'
import EventTarget from '../types/EventTarget.js'
const makeSchedule = function (runnerInfo) {
const start = runnerInfo.start
const duration = runnerInfo.runner.duration()
const end = start + duration
return {
start: start,
duration: duration,
end: end,
runner: runnerInfo.runner
}
}
const defaultSource = function () {
const w = globals.window
return (w.performance || w.Date).now()
}
export default class Timeline extends EventTarget {
// Construct a new timeline on the given element
constructor(timeSource = defaultSource) {
super()
this._timeSource = timeSource
// terminate resets all variables to their initial state
this.terminate()
}
active() {
return !!this._nextFrame
}
finish() {
// Go to end and pause
this.time(this.getEndTimeOfTimeline() + 1)
return this.pause()
}
// Calculates the end of the timeline
getEndTime() {
const lastRunnerInfo = this.getLastRunnerInfo()
const lastDuration = lastRunnerInfo ? lastRunnerInfo.runner.duration() : 0
const lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time
return lastStartTime + lastDuration
}
getEndTimeOfTimeline() {
const endTimes = this._runners.map((i) => i.start + i.runner.duration())
return Math.max(0, ...endTimes)
}
getLastRunnerInfo() {
return this.getRunnerInfoById(this._lastRunnerId)
}
getRunnerInfoById(id) {
return this._runners[this._runnerIds.indexOf(id)] || null
}
pause() {
this._paused = true
return this._continue()
}
persist(dtOrForever) {
if (dtOrForever == null) return this._persist
this._persist = dtOrForever
return this
}
play() {
// Now make sure we are not paused and continue the animation
this._paused = false
return this.updateTime()._continue()
}
reverse(yes) {
const currentSpeed = this.speed()
if (yes == null) return this.speed(-currentSpeed)
const positive = Math.abs(currentSpeed)
return this.speed(yes ? -positive : positive)
}
// schedules a runner on the timeline
schedule(runner, delay, when) {
if (runner == null) {
return this._runners.map(makeSchedule)
}
// The start time for the next animation can either be given explicitly,
// derived from the current timeline time or it can be relative to the
// last start time to chain animations directly
let absoluteStartTime = 0
const endTime = this.getEndTime()
delay = delay || 0
// Work out when to start the animation
if (when == null || when === 'last' || when === 'after') {
// Take the last time and increment
absoluteStartTime = endTime
} else if (when === 'absolute' || when === 'start') {
absoluteStartTime = delay
delay = 0
} else if (when === 'now') {
absoluteStartTime = this._time
} else if (when === 'relative') {
const runnerInfo = this.getRunnerInfoById(runner.id)
if (runnerInfo) {
absoluteStartTime = runnerInfo.start + delay
delay = 0
}
} else if (when === 'with-last') {
const lastRunnerInfo = this.getLastRunnerInfo()
const lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time
absoluteStartTime = lastStartTime
} else {
throw new Error('Invalid value for the "when" parameter')
}
// Manage runner
runner.unschedule()
runner.timeline(this)
const persist = runner.persist()
const runnerInfo = {
persist: persist === null ? this._persist : persist,
start: absoluteStartTime + delay,
runner
}
this._lastRunnerId = runner.id
this._runners.push(runnerInfo)
this._runners.sort((a, b) => a.start - b.start)
this._runnerIds = this._runners.map((info) => info.runner.id)
this.updateTime()._continue()
return this
}
seek(dt) {
return this.time(this._time + dt)
}
source(fn) {
if (fn == null) return this._timeSource
this._timeSource = fn
return this
}
speed(speed) {
if (speed == null) return this._speed
this._speed = speed
return this
}
stop() {
// Go to start and pause
this.time(0)
return this.pause()
}
time(time) {
if (time == null) return this._time
this._time = time
return this._continue(true)
}
// Remove the runner from this timeline
unschedule(runner) {
const index = this._runnerIds.indexOf(runner.id)
if (index < 0) return this
this._runners.splice(index, 1)
this._runnerIds.splice(index, 1)
runner.timeline(null)
return this
}
// Makes sure, that after pausing the time doesn't jump
updateTime() {
if (!this.active()) {
this._lastSourceTime = this._timeSource()
}
return this
}
// Checks if we are running and continues the animation
_continue(immediateStep = false) {
Animator.cancelFrame(this._nextFrame)
this._nextFrame = null
if (immediateStep) return this._stepImmediate()
if (this._paused) return this
this._nextFrame = Animator.frame(this._step)
return this
}
_stepFn(immediateStep = false) {
// Get the time delta from the last time and update the time
const time = this._timeSource()
let dtSource = time - this._lastSourceTime
if (immediateStep) dtSource = 0
const dtTime = this._speed * dtSource + (this._time - this._lastStepTime)
this._lastSourceTime = time
// Only update the time if we use the timeSource.
// Otherwise use the current time
if (!immediateStep) {
// Update the time
this._time += dtTime
this._time = this._time < 0 ? 0 : this._time
}
this._lastStepTime = this._time
this.fire('time', this._time)
// This is for the case that the timeline was seeked so that the time
// is now before the startTime of the runner. That is why we need to set
// the runner to position 0
// FIXME:
// However, resetting in insertion order leads to bugs. Considering the case,
// where 2 runners change the same attribute but in different times,
// resetting both of them will lead to the case where the later defined
// runner always wins the reset even if the other runner started earlier
// and therefore should win the attribute battle
// this can be solved by resetting them backwards
for (let k = this._runners.length; k--; ) {
// Get and run the current runner and ignore it if its inactive
const runnerInfo = this._runners[k]
const runner = runnerInfo.runner
// Make sure that we give the actual difference
// between runner start time and now
const dtToStart = this._time - runnerInfo.start
// Dont run runner if not started yet
// and try to reset it
if (dtToStart <= 0) {
runner.reset()
}
}
// Run all of the runners directly
let runnersLeft = false
for (let i = 0, len = this._runners.length; i < len; i++) {
// Get and run the current runner and ignore it if its inactive
const runnerInfo = this._runners[i]
const runner = runnerInfo.runner
let dt = dtTime
// Make sure that we give the actual difference
// between runner start time and now
const dtToStart = this._time - runnerInfo.start
// Dont run runner if not started yet
if (dtToStart <= 0) {
runnersLeft = true
continue
} else if (dtToStart < dt) {
// Adjust dt to make sure that animation is on point
dt = dtToStart
}
if (!runner.active()) continue
// If this runner is still going, signal that we need another animation
// frame, otherwise, remove the completed runner
const finished = runner.step(dt).done
if (!finished) {
runnersLeft = true
// continue
} else if (runnerInfo.persist !== true) {
// runner is finished. And runner might get removed
const endTime = runner.duration() - runner.time() + this._time
if (endTime + runnerInfo.persist < this._time) {
// Delete runner and correct index
runner.unschedule()
--i
--len
}
}
}
// Basically: we continue when there are runners right from us in time
// when -->, and when runners are left from us when <--
if (
(runnersLeft && !(this._speed < 0 && this._time === 0)) ||
(this._runnerIds.length && this._speed < 0 && this._time > 0)
) {
this._continue()
} else {
this.pause()
this.fire('finished')
}
return this
}
terminate() {
// cleanup memory
// Store the timing variables
this._startTime = 0
this._speed = 1.0
// Determines how long a runner is hold in memory. Can be a dt or true/false
this._persist = 0
// Keep track of the running animations and their starting parameters
this._nextFrame = null
this._paused = true
this._runners = []
this._runnerIds = []
this._lastRunnerId = -1
this._time = 0
this._lastSourceTime = 0
this._lastStepTime = 0
// Make sure that step is always called in class context
this._step = this._stepFn.bind(this, false)
this._stepImmediate = this._stepFn.bind(this, true)
}
}
registerMethods({
Element: {
timeline: function (timeline) {
if (timeline == null) {
this._timeline = this._timeline || new Timeline()
return this._timeline
} else {
this._timeline = timeline
return this
}
}
}
})