fieldkit
Version:
Basic building blocks for computational design projects. Written in CoffeeScript for browser and server environments.
301 lines (227 loc) • 8.48 kB
text/coffeescript
util = require './util'
math = require './math/math'
###
Timer: keeps track of time, measures intervals etc.
------------------------------------------------------------------------------
###
class Timer
now: null
prev: null
constructor: ->
@reset()
update: ->
@now = Date.now()
dt = @now - @prev
@prev = @now
dt
elapsed: -> Date.now() - @prev
reset: -> @now = @prev = Date.now()
###
Tempo: keeps track of rhythm, converts between beats, bars, time, tempo etc
------------------------------------------------------------------------------
###
class Tempo
bpm: 120
sigNum: 4
sigDenom: 4
# smallest unit in the temporal grid, how often to update in 1/n bars
resolution: 32
# internal - all times in milliseconds
beatInterval: 0
gridInterval: 0
time: 0
prevEvent: 0
# accessors - overriden by every update
beats: 0
bars: 0
beat: 0
onBeat: false
onBar: false
on64: false
on32: false
on16: false
on8: false
on4: false
on2: false
# define the tempo using a pace in BPM and a time signature
constructor: (@bpm=120, @sigNum=4, @sigDenom=4, @resolution=32) ->
@reset()
# when changing the tempo (bpm, signature) or resolution - reset needs to be called
reset: ->
# beats per millisecond
@beatInterval = 1000 / (@bpm / 60)
# user-specified grid units per millisecond
@gridInterval = @beatInterval * @sigNum / @resolution
# reset properties
@time = @prevEvent = 0
# absolute number of beats so far
@beats = 0
# current beat within a bar
@beat = @bars = 0
# booleans wether the current timestep is on that particular note
@onBeat = @onBar = false
@on64 = @on32 = @on16 = @on8 = @on4 = @on2 = false
# call update continously with time in milliseconds since last timestep
update: (dt) ->
forceOnGrid = @time - @prevEvent >= @gridInterval
@setTime @time + dt, forceOnGrid
@beat
# sets the internal clock to an absolute time in seconds
setTime: (time, forceOnGrid) ->
@time = time
# time is on grid
if @time % @gridInterval == 0 or forceOnGrid
@prevEvent = time
gridUnits = Math.floor @time / @gridInterval
u = gridUnits
r = @resolution
@beats = Math.floor @time / @beatInterval
@bars = Math.floor @beats / @sigDenom
@beat = @beats % @sigNum
@onBeat = u % (r / @sigNum) == 0
@onBar = (u % @resolution) == 0
@on64 = u % (r / 64) == 0
@on32 = u % (r / 32) == 0
@on16 = u % (r / 16) == 0
@on8 = u % (r / 8) == 0
@on4 = u % (r / 4) == 0
@on2 = u % (r / 2) == 0
# time is not on grid
else
@onBeat = @onBar = false
@on64 = @on32 = @on16 = @on8 = @on4 = @on2 = false
@beat
###
Time: Represents a single moment in time
------------------------------------------------------------------------------
###
class Time extends util.EXObject
value: 0 # time in milliseconds
# creates a new Time object from either
# - number of milliseconds
# - a time-arithmetic string (using the given fps and tempo)
# - another Time object's value
constructor: (arg, fps, tempo) ->
@set(arg, fps, tempo)
set: (arg=0, fps, tempo) ->
@value = switch typeof(arg)
when 'number' then arg
when 'string' then @eval(arg, fps, tempo)
else arg.value
# adds the given time object or millisecond value
add: (time) -> @value += if typeof(time) == 'number' then time else time.value
# adds the given time object or millisecond value and returns the result as new object
add_: (time) -> new Time(@value + if typeof(time) == 'number' then time else time.value)
# subtrct the given time object or millisecond value
sub: (time) -> @time -= if typeof(time) == 'number' then time else time.value
# subtracts the given time object or millisecond value and returns the result as new object
sub_: (time) -> new Time(@value - if typeof(time) == 'number' then time else time.value)
scale: (factor) -> @value *= factor
scale_: (factor) -> new Time(@value * factor)
clone: -> new Time(@value)
equals: (time) -> @value == if typeof(time) == 'number' then time else time.value
toString: -> "#{@value}ms"
# Returns its position relative to the given Timespan.
normalizedTo: (span) -> math.fit(@value, span.from.value, span.to.value, 0, 1)
@set 's', (seconds) -> @value = seconds * 1000
@get 's', -> @value / 1000
toFrame: (fps=60) -> Math.floor(@value / (1000 / fps))
eval: (string, fps, tempo=null) ->
# init time unit conversions
units = [
{ symbol: 'ms', factor: 1 }, # minutes
{ symbol: 's', factor: 1000 }, # seconds
{ symbol: 'm', factor: 60000 }, # minutes
# { symbol: 'h', factor: 3600000 }, # hours
{ symbol: 'f', factor: 1000 / fps } # frames
]
if tempo?
interval = tempo.gridInterval
units.push { symbol: 'i', factor: interval }
units.push { symbol: 'n', factor: interval * tempo.resolution }
# apply all unit conversions
for unit in units
# convert unit value to milliseconds
re = new RegExp "\\d+(?=#{unit.symbol})", "g"
string = string.replace re, (value) -> value * unit.factor
# remove unit symbol
re = new RegExp unit.symbol, "g"
string = string.replace re, ''
# evaluate arithmetic string
eval(string)
# creates a new Time object from the given number of seconds
@s: (seconds) -> new Time seconds * 1000
# creates a new Time object from the given number of milliseconds
@ms: (milliseconds) -> new Time milliseconds
# creates a new Time object from the given number of frames at a certain framerate
@f: (frame, fps) -> new Time frame / (1000 / fps)
# creates a new Time object from the given number of Tempo grid intervals
@i: (intervals, tempo) -> new Time intervals * tempo.gridInterval
# creates a new Time object from the given time-arithmetic string
@str: (string, fps, tempo) -> new Time string, fps, tempo
###
Timespan: Represents a duration of time between two moments
------------------------------------------------------------------------------
###
class Timespan extends util.EXObject
constructor: ->
switch arguments.length
when 0
@from = new Time(0)
@to = new Time(0)
when 1
@from = new Time(0)
@to = new Time(arguments[0])
when 2
@from = new Time(arguments[0])
@to = new Time(arguments[1])
# returns a list of Time events by stepping through this timespan along a given interval
# interval can be another time object or millisecond value
segmentByInterval: (interval, snapEnd=false) ->
interval = if typeof interval == 'number' then new Time(interval) else interval
segments = []
current = new Timespan(@from, @from.add_(interval))
segments.push current.clone()
while current.to.value < @to.value
current.from.add interval
current.to.add interval
segments.push current.clone()
# creates another segment at the
if snapEnd and segments.length > 0
last = segments[ segments.length - 1 ]
halfInterval = interval.scale_ 0.5
if @to.value - last.to.value > halfInterval
segments.push new Timespan(last.clone(), @to.clone())
else
last.to.value = @to.value
segments
# checks wether this timespan intersects with the given one
overlaps: (other) ->
from = @from.value
to = @to.value
from2 = other.from.value
to2 = other.to.value
(to2 >= from and to2 <= to) or
(from2 >= from and from2 <= to) or
(from2 <= from and to2 >= to)
clone: -> new Timespan(@from, @to)
toString: -> "Timespan(#{@from} - #{@to})"
# sets the duration
@set 'length', (length) -> @to.value = @from.value + length
@get 'length', -> new Time(@to.value - @from.value)
# return this duration in seconds
@get 's', -> @length.s
# creates a new Timespan object with a duration of the given number of seconds
@s: (seconds) -> new Timespan(seconds * 1000)
# creates a new Timespan object by parsing the given timespan arithmetic string i.e. 0..111
@str: (string, fps, tempo) ->
times = string.split '..'
throw "Invalid argument: #{string}" unless times.length == 2
from = Time.str times[0], fps, tempo
to = Time.str times[1], fps, tempo
new Timespan from, to
module.exports =
Timer: Timer
Tempo: Tempo
Time: Time
Timespan: Timespan