UNPKG

fieldkit

Version:

Basic building blocks for computational design projects. Written in CoffeeScript for browser and server environments.

301 lines (227 loc) 8.48 kB
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