• seen.coffee

  • Table of Contents

  • Apache 2.0 License
  • Init
  • Util
  • Events
  • Math
  • Colors
  • Materials
  • Lights
  • Shaders
  • Affine
  • Render Contexts
  • Painters
  • RenderModels
  • Layers
  • SVG Context
  • HTML5 Canvas Context
  • Interaction
  • Surfaces and Shapes
  • Models
  • Animator
  • Shapes
  • Camera
  • Scene
  • ¶
  • ¶

    Apache 2.0 License

    Copyright 2013, 2014 github/themadcreator
    

    Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0
    

    Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

  • ¶

    Init

    Module definition


  • ¶
  • ¶

    Declare and attach seen namespace

    seen = {}
    if window? then window.seen = seen # for the web
    if module?.exports? then module.exports = seen # for node
  • ¶

    Util

    Utility methods


  • ¶
    NEXT_UNIQUE_ID = 1 # An auto-incremented value
    
    seen.Util = {
  • ¶

    Copies default values. First, overwrite undefined attributes of obj from opts. Second, overwrite undefined attributes of obj from defaults.

      defaults: (obj, opts, defaults) ->
        for prop of opts
          if not obj[prop]? then obj[prop] = opts[prop]
        for prop of defaults
          if not obj[prop]? then obj[prop] = defaults[prop]
  • ¶

    Returns true iff the supplied Arrays are the same size and contain the same values.

      arraysEqual: (a, b) ->
        if not a.length == b.length then return false
        for val, i in a
          if not (val == b[i]) then return false
        return true
  • ¶

    Returns an ID which is unique to this instance of the library

      uniqueId: (prefix = '') ->
        return prefix + NEXT_UNIQUE_ID++
  • ¶

    Accept a DOM element or a string. If a string is provided, we assume it is the id of an element, which we return.

      element : (elementOrString) ->
        if typeof elementOrString is 'string'
          return document.getElementById(elementOrString)
        else
          return elementOrString
    }
  • ¶

    Events


  • ¶
  • ¶

    Attribution: these have been adapted from d3.js’s event dispatcher functions.

    seen.Events = {
  • ¶

    Return a new dispatcher that creates event types using the supplied string argument list. The returned Dispatcher will have methods with the names of the event types.

      dispatch : () ->
        dispatch = new seen.Events.Dispatcher()
        for arg in arguments
          dispatch[arg] = seen.Events.Event()
        return dispatch
    }
  • ¶

    The Dispatcher class. These objects have methods that can be invoked like dispatch.eventName(). Listeners can be registered with dispatch.on('eventName.uniqueId', callback). Listeners can be removed with dispatch.on('eventName.uniqueId', null). Listeners can also be registered and removed with dispatch.eventName.on('name', callback).

    Note that only one listener with the name event name and id can be registered at once. If you to generate unique ids, you can use the seen.Util.uniqueId() method.

    class seen.Events.Dispatcher
      on : (type, listener) =>
        i = type.indexOf '.'
        name = ''
        if i > 0
          name = type.substring(i + 1)
          type = type.substring(0, i)
    
        if @[type]?
          @[type].on(name, listener)
    
        return @
  • ¶

    Internal event object for storing listener callbacks and a map for easy lookup. This method returns a new event object.

    seen.Events.Event = ->
  • ¶

    Invokes all of the listeners using the supplied arguments.

      event = ->
        for name, l of event.listenerMap
          if l? then l.apply(@, arguments)
  • ¶

    Stores listeners for this event

      event.listenerMap = {}
  • ¶

    Connects a listener to the event, deleting any other listener with the same name.

      event.on = (name, listener) ->
        delete event.listenerMap[name]
        if listener? then event.listenerMap[name] = listener
    
      return event
  • ¶

    Math

    Matrices, points, and other mathy stuff


  • ¶
  • ¶

    Pool object to speed computation and reduce object creation

    ARRAY_POOL = new Array(16)
  • ¶

    Definition of identity matrix values

    IDENTITY = [1, 0, 0, 0,
                0, 1, 0, 0,
                0, 0, 1, 0,
                0, 0, 0, 1]
  • ¶

    Indices with which to transpose the matrix array

    TRANSPOSE_INDICES = [0,  4,  8, 12,
                         1,  5,  9, 13,
                         2,  6, 10, 14,
                         3,  7, 11, 15]
  • ¶

    The Matrix class stores transformations in the scene. These include: (1) Camera Projection and Viewport transformations. (2) Transformations of any Transformable type object, such as Shapes or Models

    Most of the methods on Matrix are destructive, so be sure to use .copy() when you want to preserve an object’s value.

    class seen.Matrix
  • ¶

    Accepts a 16-value Array, defaults to the identity matrix.

      constructor : (@m = null) ->
        @m ?= IDENTITY.slice()
        @baked = IDENTITY
  • ¶

    Returns a new matrix instances with a copy of the value array

      copy : ->
        return new seen.Matrix(@m.slice())
  • ¶

    Multiply by the 16-value Array argument. This method uses the ARRAY_POOL, which prevents us from having to re-initialize a new temporary matrix every time. This drastically improves performance.

      matrix : (m) ->
        c = ARRAY_POOL
        for j in [0...4]
          for i in [0...16] by 4
            c[i + j] =
              m[i    ] * @m[     j] +
              m[i + 1] * @m[ 4 + j] +
              m[i + 2] * @m[ 8 + j] +
              m[i + 3] * @m[12 + j]
        ARRAY_POOL = @m
        @m = c
        return @
  • ¶

    Resets the matrix to the baked-in (default: identity).

      reset : ->
        @m = @baked.slice()
        return @
  • ¶

    Sets the array that this matrix will return to when calling .reset(). With no arguments, it uses the current matrix state.

      bake : (m) ->
        @baked = (m ? @m).slice()
        return @
  • ¶

    Multiply by the Matrix argument.

      multiply : (b) ->
        return @matrix(b.m)
  • ¶

    Tranposes this matrix

      transpose : ->
        c = ARRAY_POOL
        for ti, i in TRANSPOSE_INDICES
          c[i] = @m[ti]
        ARRAY_POOL = @m
        @m = c
        return @
  • ¶

    Apply a rotation about the X axis. Theta is measured in Radians

      rotx : (theta) ->
        ct = Math.cos(theta)
        st = Math.sin(theta)
        rm = [ 1, 0, 0, 0, 0, ct, -st, 0, 0, st, ct, 0, 0, 0, 0, 1 ]
        return @matrix(rm)
  • ¶

    Apply a rotation about the Y axis. Theta is measured in Radians

      roty : (theta)  ->
        ct = Math.cos(theta)
        st = Math.sin(theta)
        rm = [ ct, 0, st, 0, 0, 1, 0, 0, -st, 0, ct, 0, 0, 0, 0, 1 ]
        return @matrix(rm)
  • ¶

    Apply a rotation about the Z axis. Theta is measured in Radians

      rotz : (theta) ->
        ct = Math.cos(theta)
        st = Math.sin(theta)
        rm = [ ct, -st, 0, 0, st, ct, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]
        return @matrix(rm)
  • ¶

    Apply a translation. All arguments default to 0

      translate : (x = 0, y = 0, z = 0) ->
        rm = [ 1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1 ]
        return @matrix(rm)
  • ¶

    Apply a scale. If not all arguments are supplied, each dimension (x,y,z) is copied from the previous arugment. Therefore, _scale() is equivalent to _scale(1,1,1), and _scale(1,-1) is equivalent to _scale(1,-1,-1)

      scale : (sx, sy, sz) ->
        sx ?= 1
        sy ?= sx
        sz ?= sy
        rm = [ sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1 ]
        return @matrix(rm)
  • ¶

    A convenience method for constructing Matrix objects.

    seen.M = (m) -> new seen.Matrix(m)
  • ¶

    A few useful Matrix objects.

    seen.Matrices = {
      identity : -> seen.M()
      flipX    : -> seen.M().scale(-1, 1, 1)
      flipY    : -> seen.M().scale( 1,-1, 1)
      flipZ    : -> seen.M().scale( 1, 1,-1)
    }
  • ¶

    Transformable base class extended by Shape and Model.

    The advantages of keeping transforms in Matrix form are (1) lazy computation of point position (2) ability combine hierarchical transformations easily (3) ability to reset transformations to an original state.

    Resetting transformations is especially useful when you want to animate interpolated values. Instead of computing the difference at each animation step, you can compute the global interpolated value for that time step and apply that value directly to a matrix (once it is reset).

    class seen.Transformable
      constructor: ->
        @m = new seen.Matrix()
        @baked = IDENTITY
  • ¶

    We create shims for all of the matrix transformation methods so they have the same interface.

        for method in ['scale', 'translate', 'rotx', 'roty', 'rotz', 'matrix', 'reset', 'bake'] then do (method) =>
          @[method] = ->
            @m[method].call(@m, arguments...)
            return @
  • ¶

    Apply a transformation from the supplied Matrix. see Matrix.multiply

      transform: (m) ->
        @m.multiply(m)
        return @
  • ¶

    The Point object contains x,y,z, and w coordinates. Points support various arithmetic operations with other Points, scalars, or Matrices.

    Most of the methods on Point are destructive, so be sure to use .copy() when you want to preserve an object’s value.

    class seen.Point
      constructor : (@x = 0, @y = 0, @z = 0, @w = 1) ->
  • ¶

    Creates and returns a new Point with the same values as this object.

      copy : () ->
        return new seen.Point(@x, @y, @z, @w)
  • ¶

    Copies the values of the supplied Point into this object.

      set : (p) ->
        @x = p.x
        @y = p.y
        @z = p.z
        @w = p.w
        return @
  • ¶

    Performs parameter-wise addition with the supplied Point. Excludes @w.

      add : (q) ->
        @x += q.x
        @y += q.y
        @z += q.z
        return @
  • ¶

    Performs parameter-wise subtraction with the supplied Point. Excludes @w.

      subtract : (q) ->
        @x -= q.x
        @y -= q.y
        @z -= q.z
        return @
  • ¶

    Apply a translation. Excludes @w.

      translate: (x, y, z) ->
        @x += x
        @y += y
        @z += z
        return @
  • ¶

    Multiplies each parameters by the supplied scalar value. Excludes @w.

      multiply : (n) ->
        @x *= n
        @y *= n
        @z *= n
        return @
  • ¶

    Divides each parameters by the supplied scalar value. Excludes @w.

      divide : (n) ->
        @x /= n
        @y /= n
        @z /= n
        return @
  • ¶

    Rounds each coordinate to the nearest integer. Excludes @w.

      round : () ->
        @x = Math.round(@x)
        @y = Math.round(@y)
        @z = Math.round(@z)
        return @
  • ¶

    Divides this Point by its magnitude. If the point is (0,0,0) we return (0,0,1).

      normalize : () ->
        n = @magnitude()
        if n == 0 # Strict zero comparison -- may be worth using an epsilon
          @set(seen.Points.Z())
        else
          @divide(n)
        return @
  • ¶

    Returns a new point that is perpendicular to this point

      perpendicular : () ->
        n = @copy().cross(seen.Points.Z())
        mag = n.magnitude()
        if mag isnt 0 then return n.divide(mag)
        return @copy().cross(seen.Points.X()).normalize()
  • ¶

    Apply a transformation from the supplied Matrix.

      transform : (matrix) ->
        r = POINT_POOL
        r.x = @x * matrix.m[0]  + @y * matrix.m[1]  + @z * matrix.m[2]  + @w * matrix.m[3]
        r.y = @x * matrix.m[4]  + @y * matrix.m[5]  + @z * matrix.m[6]  + @w * matrix.m[7]
        r.z = @x * matrix.m[8]  + @y * matrix.m[9]  + @z * matrix.m[10] + @w * matrix.m[11]
        r.w = @x * matrix.m[12] + @y * matrix.m[13] + @z * matrix.m[14] + @w * matrix.m[15]
    
        @set(r)
        return @
  • ¶

    Returns this Points magnitude squared. Excludes @w.

      magnitudeSquared : () ->
        return @dot(@)
  • ¶

    Returns this Points magnitude. Excludes @w.

      magnitude : () ->
        return Math.sqrt(@magnitudeSquared())
  • ¶

    Computes the dot product with the supplied Point.

      dot : (q) ->
        return @x * q.x + @y * q.y + @z * q.z
  • ¶

    Computes the cross product with the supplied Point.

      cross : (q) ->
        r = POINT_POOL
        r.x = @y * q.z - @z * q.y
        r.y = @z * q.x - @x * q.z
        r.z = @x * q.y - @y * q.x
    
        @set(r)
        return @
  • ¶

    Convenience method for creating Points.

    seen.P = (x,y,z,w) -> new seen.Point(x,y,z,w)
  • ¶

    A pool object which prevents us from having to create new Point objects for various calculations, which vastly improves performance.

    POINT_POOL = seen.P()
  • ¶

    A few useful Point objects. Be sure that you don’t invoke destructive methods on these objects.

    seen.Points = {
      X    : -> seen.P(1, 0, 0)
      Y    : -> seen.P(0, 1, 0)
      Z    : -> seen.P(0, 0, 1)
      ZERO : -> seen.P(0, 0, 0)
    }
  • ¶

    A Quaterionion class for computing quaterion multiplications. This creates more natural mouse rotations.

    Attribution: adapted from http://glprogramming.com/codedump/godecho/quaternion.html

    class seen.Quaternion
      @pixelsPerRadian : 150
  • ¶

    Convert the x and y pixel offsets into a rotation matrix

      @xyToTransform : (x, y) ->
        quatX = seen.Quaternion.pointAngle(seen.Points.Y(), x / seen.Quaternion.pixelsPerRadian)
        quatY = seen.Quaternion.pointAngle(seen.Points.X(), y / seen.Quaternion.pixelsPerRadian)
        return quatX.multiply(quatY).toMatrix()
  • ¶

    Create a rotation matrix from the axis defined by x, y, and z values, and the supplied angle.

      @axisAngle : (x, y, z, angleRads) ->
        scale = Math.sin(angleRads / 2.0)
        w     = Math.cos(angleRads / 2.0)
        return new seen.Quaternion(scale * x, scale * y, scale * z, w)
  • ¶

    Create a rotation matrix from the axis defined by the supplied point and the supplied angle.

      @pointAngle : (p, angleRads) ->
        scale = Math.sin(angleRads / 2.0)
        w     = Math.cos(angleRads / 2.0)
        return new seen.Quaternion(scale * p.x, scale * p.y, scale * p.z, w)
    
      constructor : ->
        @q = seen.P(arguments...)
  • ¶

    Multiply this Quaterionion by the Quaternion argument.

      multiply : (q) ->
        r = seen.P()
    
        r.w = @q.w * q.q.w - @q.x * q.q.x - @q.y * q.q.y - @q.z * q.q.z
        r.x = @q.w * q.q.x + @q.x * q.q.w + @q.y * q.q.z - @q.z * q.q.y
        r.y = @q.w * q.q.y + @q.y * q.q.w + @q.z * q.q.x - @q.x * q.q.z
        r.z = @q.w * q.q.z + @q.z * q.q.w + @q.x * q.q.y - @q.y * q.q.x
    
        result = new seen.Quaternion()
        result.q = r
        return result
  • ¶

    Convert this Quaterion into a transformation matrix.

      toMatrix : ->
        m = new Array(16)
    
        m[ 0] = 1.0 - 2.0 * ( @q.y * @q.y + @q.z * @q.z )
        m[ 1] = 2.0 * ( @q.x * @q.y - @q.w * @q.z )
        m[ 2] = 2.0 * ( @q.x * @q.z + @q.w * @q.y )
        m[ 3] = 0.0
    
        m[ 4] = 2.0 * ( @q.x * @q.y + @q.w * @q.z )
        m[ 5] = 1.0 - 2.0 * ( @q.x * @q.x + @q.z * @q.z )
        m[ 6] = 2.0 * ( @q.y * @q.z - @q.w * @q.x )
        m[ 7] = 0.0
    
        m[ 8] = 2.0 * ( @q.x * @q.z - @q.w * @q.y )
        m[ 9] = 2.0 * ( @q.y * @q.z + @q.w * @q.x )
        m[10] = 1.0 - 2.0 * ( @q.x * @q.x + @q.y * @q.y )
        m[11] = 0.0
    
        m[12] = 0
        m[13] = 0
        m[14] = 0
        m[15] = 1.0
        return seen.M(m)
  • ¶

    The Bounds object contains an axis-aligned bounding box.

    class seen.Bounds
    
      @points : (points) ->
        box = new seen.Bounds()
        box.add(p) for p in points
        return box
    
      @xywh : (x, y, w, h) ->
        return seen.Boundses.xyzwhd(x, y, 0, w, h, 0)
    
      @xyzwhd : (x, y, z, w, h, d) ->
        box = new seen.Bounds()
        box.add(seen.P(x, y, z))
        box.add(seen.P(x+w, y+h, z+d))
        return box
    
      constructor : () ->
        @min = null
        @max = null
  • ¶

    Creates a copy of this box object with the same bounds

      copy : () ->
        box = new seen.Bounds()
        box.min = @min?.copy()
        box.max = @max?.copy()
        return box
  • ¶

    Adds this point to the bounding box, extending it if necessary

      add : (p) ->
        if not (@min? and @max?)
          @min = p.copy()
          @max = p.copy()
        else
          @min.x = Math.min(@min.x, p.x)
          @min.y = Math.min(@min.y, p.y)
          @min.z = Math.min(@min.z, p.z)
    
          @max.x = Math.max(@max.x, p.x)
          @max.y = Math.max(@max.y, p.y)
          @max.z = Math.max(@max.z, p.z)
        return @
  • ¶

    Returns true of this box contains at least one point

      valid : ->
        return (@min? and @max?)
  • ¶

    Trims this box so that it results in the intersection of this box and the supplied box.

      intersect : (box) ->
        if not @valid() or not box.valid()
          @min = null
          @max = null
        else
          @min = seen.P(
            Math.max(@min.x, box.min.x)
            Math.max(@min.y, box.min.y)
            Math.max(@min.z, box.min.z)
          )
          @max = seen.P(
            Math.min(@max.x, box.max.x)
            Math.min(@max.y, box.max.y)
            Math.min(@max.z, box.max.z)
          )
          if @min.x > @max.x or @min.y > @max.y or @min.z > @max.z
            @min = null
            @max = null
        return @
  • ¶

    Pads the min and max of this box using the supplied x, y, and z

      pad : (x, y, z) ->
        if @valid()
          y ?= x
          z ?= y
          p  = seen.P(x,y,z)
          @min.subtract(p)
          @max.add(p)
        return @
  • ¶

    Returns this bounding box to an empty state

      reset : () ->
        @min = null
        @max = null
        return @
  • ¶

    Return true iff the point p lies within this bounding box. Points on the edge of the box are included.

      contains : (p) ->
        if not @valid()
          return false
        else if @min.x > p.x or @max.x < p.x
          return false
        else if @min.y > p.y or @max.y < p.y
          return false
        else if @min.z > p.z or @max.z < p.z
          return false
        else
          return true
  • ¶

    Returns the center of the box or zero if no points are in the box

      center : () ->
        return seen.P(
          @minX() + @width()/2
          @minY() + @height()/2
          @minZ() + @depth()/2
        )
  • ¶

    Returns the width (x extent) of the box

      width  : () => @maxX() - @minX()
  • ¶

    Returns the height (y extent) of the box

      height : () => @maxY() - @minY()
  • ¶

    Returns the depth (z extent) of the box

      depth  : () => @maxZ() - @minZ()
    
      minX : () => return @min?.x ? 0
      minY : () => return @min?.y ? 0
      minZ : () => return @min?.z ? 0
    
      maxX : () => return @max?.x ? 0
      maxY : () => return @max?.y ? 0
      maxZ : () => return @max?.z ? 0
  • ¶

    Colors


  • ¶
  • ¶

    Color objects store RGB and Alpha values from 0 to 255.

    class seen.Color
      constructor : (@r = 0, @g = 0, @b = 0, @a = 0xFF) ->
  • ¶

    Returns a new Color object with the same rgb and alpha values as the current object

      copy : () ->
        return new seen.Color(@r, @g, @b, @a)
  • ¶

    Scales the rgb channels by the supplied scalar value.

      scale : (n) ->
        @r *= n
        @g *= n
        @b *= n
        return @
  • ¶

    Offsets each rgb channel by the supplied scalar value.

      offset : (n) ->
        @r += n
        @g += n
        @b += n
        return @
  • ¶

    Clamps each rgb channel to the supplied minimum and maximum scalar values.

      clamp : (min = 0, max = 0xFF) ->
        @r = Math.min(max, Math.max(min, @r))
        @g = Math.min(max, Math.max(min, @g))
        @b = Math.min(max, Math.max(min, @b))
        return @
  • ¶

    Takes the minimum between each channel of this Color and the supplied Color object.

      minChannels : (c) ->
        @r = Math.min(c.r, @r)
        @g = Math.min(c.g, @g)
        @b = Math.min(c.b, @b)
        return @
  • ¶

    Adds the channels of the current Color with each respective channel from the supplied Color object.

      addChannels : (c) ->
        @r += c.r
        @g += c.g
        @b += c.b
        return @
  • ¶

    Multiplies the channels of the current Color with each respective channel from the supplied Color object.

      multiplyChannels : (c) ->
        @r *= c.r
        @g *= c.g
        @b *= c.b
        return @
  • ¶

    Converts the Color into a hex string of the form “#RRGGBB”.

      hex : () ->
        c = (@r << 16 | @g << 8 | @b).toString(16)
        while (c.length < 6) then c = '0' + c
        return '#' + c
  • ¶

    Converts the Color into a CSS-style string of the form “rgba(RR, GG, BB, AA)”

      style : () ->
        return "rgba(#{@r},#{@g},#{@b},#{@a})"
    
    seen.Colors = {
      CSS_RGBA_STRING_REGEX : /rgb(a)?\(([0-9.]+),([0-9.]+),*([0-9.]+)(,([0-9.]+))?\)/
  • ¶

    Parses a hex string starting with an octothorpe (#) or an rgb/rgba CSS string. Note that the CSS rgba format uses a float value of 0-1.0 for alpha, but seen uses an in from 0-255.

      parse : (str) ->
        if str.charAt(0) is '#' and str.length is 7
          return seen.Colors.hex(str)
        else if str.indexOf('rgb') is 0
          m = seen.Colors.CSS_RGBA_STRING_REGEX.exec(str)
          return seen.Colors.black() unless m?
          a = if m[6]? then Math.round(parseFloat(m[6]) * 0xFF) else undefined
          return new seen.Color(parseFloat(m[2]), parseFloat(m[3]), parseFloat(m[4]), a)
        else
          return seen.Colors.black()
  • ¶

    Creates a new Color using the supplied rgb and alpha values.

    Each value must be in the range [0, 255] or, equivalently, [0x00, 0xFF].

      rgb : (r, g, b, a = 255) ->
        return new seen.Color(r, g, b, a)
  • ¶

    Creates a new Color using the supplied hex string of the form “#RRGGBB”.

      hex : (hex) ->
        hex = hex.substring(1) if (hex.charAt(0) is '#')
        return new seen.Color(
          parseInt(hex.substring(0, 2), 16),
          parseInt(hex.substring(2, 4), 16),
          parseInt(hex.substring(4, 6), 16))
  • ¶

    Creates a new Color using the supplied hue, saturation, and lightness (HSL) values.

    Each value must be in the range [0.0, 1.0].

      hsl : (h, s, l, a = 1) ->
        r = g = b = 0
        if (s == 0)
  • ¶

    When saturation is 0, the color is “achromatic” or “grayscale”.

          r = g = b = l
        else
          hue2rgb = (p, q, t) ->
            if (t < 0)
              t += 1
             else if (t > 1)
              t -= 1
    
            if (t < 1 / 6)
              return p + (q - p) * 6 * t
            else if (t < 1 / 2)
              return q
            else if (t < 2 / 3)
              return p + (q - p) * (2 / 3 - t) * 6
            else
              return p
    
          q = if l < 0.5 then l * (1 + s) else l + s - l * s
          p = 2 * l - q
          r = hue2rgb(p, q, h + 1 / 3)
          g = hue2rgb(p, q, h)
          b = hue2rgb(p, q, h - 1 / 3)
    
        return new seen.Color(r * 255, g * 255, b * 255, a * 255)
  • ¶

    Generates a new random color for each surface of the supplied Shape.

      randomSurfaces : (shape, sat = 0.5, lit = 0.4) ->
        for surface in shape.surfaces
          surface.fill seen.Colors.hsl(Math.random(), sat, lit)
  • ¶

    Generates a random hue then randomly drifts the hue for each surface of the supplied Shape.

      randomSurfaces2 : (shape, drift = 0.03, sat = 0.5, lit = 0.4) ->
        hue = Math.random()
        for surface in shape.surfaces
          hue += (Math.random() - 0.5) * drift
          while hue < 0 then hue += 1
          while hue > 1 then hue -= 1
          surface.fill seen.Colors.hsl(hue, 0.5, 0.4)
  • ¶

    Generates a random color then sets the fill for every surface of the supplied Shape.

      randomShape : (shape, sat = 0.5, lit = 0.4) ->
        shape.fill new seen.Material seen.Colors.hsl(Math.random(), sat, lit)
  • ¶

    A few Colors are supplied for convenience.

      black : -> @hex('#000000')
      white : -> @hex('#FFFFFF')
      gray  : -> @hex('#888888')
    }
  • ¶

    Convenience Color constructor.

    seen.C = (r,g,b,a) -> new seen.Color(r,g,b,a)
  • ¶

    Materials

    Surface material properties


  • ¶
  • ¶

    Material objects hold the attributes that desribe the color and finish of a surface.

    class seen.Material
      @create : (value) ->
        if value instanceof seen.Material
          return value
        else if value instanceof seen.Color
          return new seen.Material(value)
        else if typeof value is 'string'
          return new seen.Material(seen.Colors.parse(value))
        else
          return new seen.Material()
    
      defaults :
  • ¶

    The base color of the material.

        color            : seen.Colors.gray()
  • ¶

    The metallic attribute determines how the specular highlights are calculated. Normally, specular highlights are the color of the light source. If metallic is true, specular highlight colors are determined from the specularColor attribute.

        metallic         : false
  • ¶

    The color used for specular highlights when metallic is true.

        specularColor    : seen.Colors.white()
  • ¶

    The specularExponent determines how “shiny” the material is. A low exponent will create a low-intesity, diffuse specular shine. A high exponent will create an intense, point-like specular shine.

        specularExponent : 15
  • ¶

    A Shader object may be supplied to override the shader used for this material. For example, if you want to apply a flat color to text or other shapes, set this value to seen.Shaders.Flat.

        shader           : null
    
      constructor : (@color, options = {}) ->
        seen.Util.defaults(@, options, @defaults)
  • ¶

    Apply the shader’s shading to this material, with the option to override the shader with the material’s shader (if defined).

      render : (lights, shader, renderData) ->
        renderShader = @shader ? shader
        color = renderShader.shade(lights, renderData, @)
        color.a = @color.a
        return color
  • ¶

    Lights


  • ¶
  • ¶

    This model object holds the attributes and transformation of a light source.

    class seen.Light extends seen.Transformable
      defaults :
        point     : seen.P()
        color     : seen.Colors.white()
        intensity : 0.01
        normal    : seen.P(1, -1, -1).normalize()
        enabled   : true
    
      constructor: (@type, options) ->
        super
        seen.Util.defaults(@, options, @defaults)
        @id = seen.Util.uniqueId('l')
    
      render : ->
        @colorIntensity = @color.copy().scale(@intensity)
    
    seen.Lights = {
  • ¶

    A point light emits light eminating in all directions from a single point. The point property determines the location of the point light. Note, though, that it may also be moved through the transformation of the light.

      point       : (opts) -> new seen.Light 'point', opts
  • ¶

    A directional lights emit light in parallel lines, not eminating from any single point. For these lights, only the normal property is used to determine the direction of the light. This may also be transformed.

      directional : (opts) -> new seen.Light 'directional', opts
  • ¶

    Ambient lights emit a constant amount of light everywhere at once. Transformation of the light has no effect.

      ambient     : (opts) -> new seen.Light 'ambient', opts
    }
  • ¶

    Shaders


  • ¶
    EYE_NORMAL = seen.Points.Z()
  • ¶

    These shading functions compute the shading for a surface. To reduce code duplication, we aggregate them in a utils object.

    seen.ShaderUtils = {
      applyDiffuse : (c, light, lightNormal, surfaceNormal, material) ->
        dot = lightNormal.dot(surfaceNormal)
    
        if (dot > 0)
  • ¶

    Apply diffuse phong shading

          c.addChannels(light.colorIntensity.copy().scale(dot))
    
      applyDiffuseAndSpecular : (c, light, lightNormal, surfaceNormal, material) ->
        dot = lightNormal.dot(surfaceNormal)
    
        if (dot > 0)
  • ¶

    Apply diffuse phong shading

          c.addChannels(light.colorIntensity.copy().scale(dot))
  • ¶

    Compute and apply specular phong shading

          reflectionNormal  = surfaceNormal.copy().multiply(dot * 2).subtract(lightNormal)
          specularIntensity = Math.pow(0.5 + reflectionNormal.dot(EYE_NORMAL), material.specularExponent)
          specularColor     = material.specularColor.copy().scale(specularIntensity * light.intensity / 255.0)
          c.addChannels(specularColor)
    
      applyAmbient : (c, light) ->
  • ¶

    Apply ambient shading

        c.addChannels(light.colorIntensity)
    }
  • ¶

    The Shader class is the base class for all shader objects.

    class seen.Shader
  • ¶

    Every Shader implementation must override the shade method.

    lights is an object containing the ambient, point, and directional light sources. renderModel is an instance of RenderModel and contains the transformed and projected surface data. material is an instance of Material and contains the color and other attributes for determining how light reflects off the surface.

      shade: (lights, renderModel, material) -> # Override this
  • ¶

    The Phong shader implements the Phong shading model with a diffuse, specular, and ambient term.

    See https://en.wikipedia.org/wiki/Phong_reflection_model for more information

    class Phong extends seen.Shader
      shade: (lights, renderModel, material) ->
        c = new seen.Color()
    
        for light in lights
          switch light.type
            when 'point'
              lightNormal = light.point.copy().subtract(renderModel.barycenter).normalize()
              seen.ShaderUtils.applyDiffuseAndSpecular(c, light, lightNormal, renderModel.normal, material)
            when 'directional'
              seen.ShaderUtils.applyDiffuseAndSpecular(c, light, light.normal, renderModel.normal, material)
            when 'ambient'
              seen.ShaderUtils.applyAmbient(c, light)
    
        c.multiplyChannels(material.color)
    
        if material.metallic
          c.minChannels(material.specularColor)
    
        c.clamp(0, 0xFF)
        return c
  • ¶

    The DiffusePhong shader implements the Phong shading model with a diffuse and ambient term (no specular).

    class DiffusePhong extends seen.Shader
      shade: (lights, renderModel, material) ->
        c = new seen.Color()
    
        for light in lights
          switch light.type
            when 'point'
              lightNormal = light.point.copy().subtract(renderModel.barycenter).normalize()
              seen.ShaderUtils.applyDiffuse(c, light, lightNormal, renderModel.normal, material)
            when 'directional'
              seen.ShaderUtils.applyDiffuse(c, light, light.normal, renderModel.normal, material)
            when 'ambient'
              seen.ShaderUtils.applyAmbient(c, light)
    
        c.multiplyChannels(material.color).clamp(0, 0xFF)
        return c
  • ¶

    The Ambient shader colors surfaces from ambient light only.

    class Ambient extends seen.Shader
      shade: (lights, renderModel, material) ->
        c = new seen.Color()
    
        for light in lights
          switch light.type
            when 'ambient'
              seen.ShaderUtils.applyAmbient(c, light)
    
        c.multiplyChannels(material.color).clamp(0, 0xFF)
        return c
  • ¶

    The Flat shader colors surfaces with the material color, disregarding all light sources.

    class Flat extends seen.Shader
      shade: (lights, renderModel, material) ->
        return material.color
    
    seen.Shaders = {
      phong   : -> new Phong()
      diffuse : -> new DiffusePhong()
      ambient : -> new Ambient()
      flat    : -> new Flat()
    }
  • ¶

    Affine

    Fake projections with affine transforms


  • ¶

    It is not possible exactly render text in a scene with a perspective projection because Canvas and SVG support only affine transformations. So, in order to fake it, we create an affine transform that approximates the linear effects of a perspective projection on an unrendered planar surface that represents the text’s shape. We can use this transform directly in the text painter to warp the text.

    This fake projection will produce unrealistic results with large strings of text that are not broken into their own shapes.

    seen.Affine = {
  • ¶

    This is the set of points that must be used by a surface that will use an affine transform for rendering.

      ORTHONORMAL_BASIS : -> [
        seen.P( 0, 0,  0)
        seen.P(20, 0,  0)
        seen.P( 0, 20, 0)
      ]
  • ¶

    This matrix is built using the method from this StackOverflow answer: http://stackoverflow.com/questions/22954239/given-three-points-compute-affine-transformation

    We further re-arranged the rows to avoid having to do any matrix factorization.

      INITIAL_STATE_MATRIX : [
        [20,  0, 1,  0,  0, 0]
        [ 0, 20, 1,  0,  0, 0]
        [ 0,  0, 1,  0,  0, 0]
        [ 0,  0, 0, 20,  0, 1]
        [ 0,  0, 0,  0, 20, 1]
        [ 0,  0, 0,  0,  0, 1]
      ]
  • ¶

    Computes the parameters of an affine transform from the 3 projected points.

    Because we control the initial values of the points, we can re-use the state matrix. Furthermore, because we have use a special layout (upper triangular) for this matrix, we avoid any matrix factorization and can go directly to back-substitution to solve the matrix equation.

    To use the affine transform, use the indices like so (note that we flip y): x[0], x[3], -x[1], -x[4], x[2], x[5]

      solveForAffineTransform : (points) ->
        A = seen.Affine.INITIAL_STATE_MATRIX
    
        b = [
          points[1].x
          points[2].x
          points[0].x
          points[1].y
          points[2].y
          points[0].y
        ]
  • ¶

    Use back substitution to solve A*x=b for x

        x = new Array(6)
        n = A.length
        for i in [(n-1)..0] by -1
          x[i] = b[i]
          for j in [(i+1)...n]
            x[i] -= A[i][j] * x[j]
          x[i] /= A[i][i]
    
        return x
    }
  • ¶

    Render Contexts


  • ¶
  • ¶

    The RenderContext uses RenderModels produced by the scene’s render method to paint the shapes into an HTML element. Since we support both SVG and Canvas painters, the RenderContext and RenderLayerContext define a common interface.

    class seen.RenderContext
      constructor: ->
        @layers = []
    
      render: () =>
        @reset()
        for layer in @layers
          layer.context.reset()
          layer.layer.render(layer.context)
          layer.context.cleanup()
        @cleanup()
        return @
  • ¶

    Returns a new Animator with this context’s render method pre-registered.

      animate : ->
        return new seen.RenderAnimator(@)
  • ¶

    Add a new RenderLayerContext to this context. This allows us to easily stack paintable components such as a fill backdrop, or even multiple scenes in one context.

      layer: (layer) ->
        @layers.push {
          layer   : layer
          context : @
        }
        return @
    
      sceneLayer : (scene) ->
        @layer(new seen.SceneLayer(scene))
        return @
    
      reset   : ->
      cleanup : ->
  • ¶

    The RenderLayerContext defines the interface for producing painters that can paint various things into the current layer.

    class seen.RenderLayerContext
      path    : -> # Return a path painter
      rect    : -> # Return a rect painter
      circle  : -> # Return a circle painter
      text    : -> # Return a text painter
    
      reset   : ->
      cleanup : ->
  • ¶

    Create a render context for the element with the specified elementId. elementId should correspond to either an SVG or Canvas element.

    seen.Context = (elementId, scene = null) ->
      tag = seen.Util.element(elementId)?.tagName.toUpperCase()
      context = switch tag
        when 'SVG', 'G' then new seen.SvgRenderContext(elementId)
        when 'CANVAS'   then new seen.CanvasRenderContext(elementId)
      if context? and scene?
        context.sceneLayer(scene)
      return context
  • ¶

    Painters

    Surface painters


  • ¶
  • ¶

    Each Painter overrides the paint method. It uses the supplied RenderLayerContext‘s builders to create and style the geometry on screen.

    class seen.Painter
      paint : (renderModel, context) ->
    
    class seen.PathPainter extends seen.Painter
      paint : (renderModel, context) ->
        painter = context.path().path(renderModel.projected.points)
    
        if renderModel.fill?
          painter.fill(
            fill           : if not renderModel.fill? then 'none' else renderModel.fill.hex()
            'fill-opacity' : if not renderModel.fill?.a? then 1.0 else (renderModel.fill.a / 255.0)
          )
    
        if renderModel.stroke?
          painter.draw(
            fill           : 'none'
            stroke         : if not renderModel.stroke? then 'none' else renderModel.stroke.hex()
            'stroke-width' : renderModel.surface['stroke-width'] ? 1
          )
    
    class seen.TextPainter extends seen.Painter
      paint : (renderModel, context) ->
        style = {
          fill          : if not renderModel.fill? then 'none' else renderModel.fill.hex()
          font          : renderModel.surface.font
          'text-anchor' : renderModel.surface.anchor ? 'middle'
        }
        xform = seen.Affine.solveForAffineTransform(renderModel.projected.points)
        context.text().fillText(xform, renderModel.surface.text, style)
    
    seen.Painters = {
      path : new seen.PathPainter()
      text : new seen.TextPainter()
    }
  • ¶

    RenderModels


  • ¶
    DEFAULT_NORMAL = seen.Points.Z()
  • ¶

    The RenderModel object contains the transformed and projected points as well as various data needed to shade and paint a Surface.

    Once initialized, the object will have a constant memory footprint down to Number primitives. Also, we compare each transform and projection to prevent unnecessary re-computation.

    If you need to force a re-computation, mark the surface as ‘dirty’.

    class seen.RenderModel
      constructor: (@surface, @transform, @projection, @viewport) ->
        @points      = @surface.points
        @transformed = @_initRenderData()
        @projected   = @_initRenderData()
        @_update()
    
      update: (transform, projection, viewport) ->
        if not @surface.dirty and seen.Util.arraysEqual(transform.m, @transform.m) and seen.Util.arraysEqual(projection.m, @projection.m) and seen.Util.arraysEqual(viewport.m, @viewport.m)
          return
        else
          @transform  = transform
          @projection = projection
          @viewport   = viewport
          @_update()
    
      _update: () ->
  • ¶

    Apply model transforms to surface points

        @_math(@transformed, @points, @transform, false)
  • ¶

    Project into camera space

        cameraSpace = @transformed.points.map (p) => p.copy().transform(@projection)
        @inFrustrum = @_checkFrustrum(cameraSpace)
  • ¶

    Project into screen space

        @_math(@projected, cameraSpace, @viewport, true)
        @surface.dirty = false
    
      _checkFrustrum : (points) ->
        for p in points
          return false if (p.z <= -2)
        return true
    
      _initRenderData: ->
        return {
          points     : (p.copy() for p in @points)
          bounds     : new seen.Bounds()
          barycenter : seen.P()
          normal     : seen.P()
          v0         : seen.P()
          v1         : seen.P()
        }
    
      _math: (set, points, transform, applyClip = false) ->
  • ¶

    Apply transform to points

        for p,i in points
          sp = set.points[i]
          sp.set(p).transform(transform)
  • ¶

    Applying the clip is what ultimately scales the x and y coordinates in a perpsective projection

          if applyClip then sp.divide(sp.w)
  • ¶

    Compute barycenter, which is used in aligning shapes in the painters algorithm

        set.barycenter.multiply(0)
        for p in set.points
          set.barycenter.add(p)
        set.barycenter.divide(set.points.length)
  • ¶

    Compute the bounding box of the points

        set.bounds.reset()
        for p in set.points
          set.bounds.add(p)
  • ¶

    Compute normal, which is used for backface culling (when enabled)

        if set.points.length < 2
          set.v0.set(DEFAULT_NORMAL)
          set.v1.set(DEFAULT_NORMAL)
          set.normal.set(DEFAULT_NORMAL)
        else
          set.v0.set(set.points[1]).subtract(set.points[0])
          set.v1.set(set.points[points.length - 1]).subtract(set.points[0])
          set.normal.set(set.v0).cross(set.v1).normalize()
  • ¶

    The LightRenderModel stores pre-computed values necessary for shading surfaces with the supplied Light.

    class seen.LightRenderModel
      constructor: (@light, transform) ->
        @colorIntensity = @light.color.copy().scale(@light.intensity)
        @type           = @light.type
        @intensity      = @light.intensity
        @point          = @light.point.copy().transform(transform)
        origin          = seen.Points.ZERO().transform(transform)
        @normal         = @light.normal.copy().transform(transform).subtract(origin).normalize()
  • ¶

    Layers


  • ¶
    class seen.RenderLayer
      render: (context) =>
    
    class seen.SceneLayer extends seen.RenderLayer
      constructor : (@scene) ->
    
      render : (context) =>
        for renderModel in @scene.render()
          renderModel.surface.painter.paint(renderModel, context)
    
    class seen.FillLayer extends seen.RenderLayer
      constructor : (@width = 500, @height = 500, @fill = '#EEE') ->
    
      render: (context) =>
        context.rect()
          .rect(@width, @height)
          .fill(fill : @fill)
  • ¶

    SVG Context


  • ¶
  • ¶

    Creates a new SVG element in the SVG namespace.

    _svg = (name) ->
      return document.createElementNS('http://www.w3.org/2000/svg', name)
    
    class seen.SvgStyler
      _attributes : {}
      _svgTag     : 'g'
    
      constructor : (@elementFactory) ->
    
      clear : () ->
        @_attributes = {}
        return @
    
      fill : (style = {}) ->
        @_paint(style)
        return @
    
      draw : (style = {}) ->
        @_paint(style)
        return @
    
      _paint : (style) ->
        el = @elementFactory(@_svgTag)
    
        str = ''
        for key, value of style
          str += "#{key}:#{value};"
        el.setAttribute('style', str)
    
        for key, value of @_attributes
          el.setAttribute(key, value)
        return el
    
    class seen.SvgPathPainter extends seen.SvgStyler
      _svgTag : 'path'
    
      path : (points) ->
        @_attributes.d = 'M' + points.map((p) -> "#{p.x} #{p.y}").join 'L'
        return @
    
    class seen.SvgTextPainter
      _svgTag      : 'text'
    
      constructor : (@elementFactory) ->
    
      fillText : (m, text, style = {}) ->
        el = @elementFactory(@_svgTag)
        el.setAttribute('transform', "matrix(#{m[0]} #{m[3]} #{-m[1]} #{-m[4]} #{m[2]} #{m[5]})")
    
        str = ''
        for key, value of style
          if value? then str += "#{key}:#{value};"
        el.setAttribute('style', str)
    
        el.textContent = text
    
    
    class seen.SvgRectPainter extends seen.SvgStyler
      _svgTag : 'rect'
    
      rect : (width, height) ->
        @_attributes.width  = width
        @_attributes.height = height
        return @
    
    class seen.SvgCirclePainter extends seen.SvgStyler
      _svgTag : 'circle'
    
      circle: (center, radius) ->
        @_attributes.cx = center.x
        @_attributes.cy = center.y
        @_attributes.r  = radius
        return @
    
    class seen.SvgLayerRenderContext extends seen.RenderLayerContext
      constructor : (@group) ->
        @pathPainter   = new seen.SvgPathPainter(@_elementFactory)
        @textPainter   = new seen.SvgTextPainter(@_elementFactory)
        @circlePainter = new seen.SvgCirclePainter(@_elementFactory)
        @rectPainter   = new seen.SvgRectPainter(@_elementFactory)
        @_i = 0
    
      path   : () -> @pathPainter.clear()
      rect   : () -> @rectPainter.clear()
      circle : () -> @circlePainter.clear()
      text   : () -> @textPainter
    
      reset : ->
        @_i = 0
    
      cleanup : ->
        children = @group.childNodes
        while (@_i < children.length)
          children[@_i].setAttribute('style', 'display: none;')
          @_i++
  • ¶

    Returns an element with tagname type.

    This method uses an internal iterator to add new elements as they are drawn. If there is no child element at the current index, we append one and return it. If an element exists at the current index and it is the same type, we return that. If the element is a different type, we create one and replace it then return it.

      _elementFactory : (type) =>
        children = @group.childNodes
        if @_i >= children.length
          path = _svg(type)
          @group.appendChild(path)
          @_i++
          return path
    
        current = children[@_i]
        if current.tagName is type
          @_i++
          return current
        else
          path = _svg(type)
          @group.replaceChild(path, current)
          @_i++
          return path
    
    class seen.SvgRenderContext extends seen.RenderContext
      constructor : (@svg) ->
        super()
        @svg = seen.Util.element(@svg)
    
      layer : (layer) ->
        @svg.appendChild(group = _svg('g'))
        @layers.push {
          layer   : layer
          context : new seen.SvgLayerRenderContext(group)
        }
        return @
    
    seen.SvgContext = (elementId, scene) ->
      context = new seen.SvgRenderContext(elementId)
      if scene? then context.sceneLayer(scene)
      return context
  • ¶

    HTML5 Canvas Context


  • ¶
    class seen.CanvasStyler
      constructor : (@ctx) ->
    
      draw : (style = {}) ->
  • ¶

    Copy over SVG CSS attributes

        if style.stroke? then @ctx.strokeStyle = style.stroke
        if style['stroke-width']? then @ctx.lineWidth = style['stroke-width']
        if style['text-anchor']? then @ctx.textAlign = style['text-anchor']
    
        @ctx.stroke()
        return @
    
      fill : (style = {}) ->
  • ¶

    Copy over SVG CSS attributes

        if style.fill? then @ctx.fillStyle = style.fill
        if style['text-anchor']? then @ctx.textAlign = style['text-anchor']
        if style['fill-opacity'] then @ctx.globalAlpha = style['fill-opacity']
    
        @ctx.fill()
        return @
    
    class seen.CanvasPathPainter extends seen.CanvasStyler
      path: (points) ->
        @ctx.beginPath()
    
        for p, i in points
          if i is 0
            @ctx.moveTo(p.x, p.y)
          else
            @ctx.lineTo(p.x, p.y)
    
        @ctx.closePath()
        return @
    
    class seen.CanvasRectPainter extends seen.CanvasStyler
      rect: (width, height) ->
        @ctx.rect(0, 0, width, height)
        return @
    
    class seen.CanvasCirclePainter extends seen.CanvasStyler
      circle: (center, radius) ->
        @ctx.beginPath()
        @ctx.arc(center.x, center.y, radius, 0, 2*Math.PI, true)
        return @
    
    class seen.CanvasTextPainter
      constructor : (@ctx) ->
    
      fillText : (m, text, style = {}) ->
        @ctx.save()
        @ctx.setTransform(m[0], m[3], -m[1], -m[4], m[2], m[5])
    
        if style.font? then @ctx.font = style.font
        if style.fill? then @ctx.fillStyle = style.fill
        if style['text-anchor']? then @ctx.textAlign = @_cssToCanvasAnchor(style['text-anchor'])
    
        @ctx.fillText(text, 0, 0)
        @ctx.restore()
        return @
    
      _cssToCanvasAnchor : (anchor) ->
        if anchor is 'middle' then return 'center'
        return anchor
    
    class seen.CanvasLayerRenderContext extends seen.RenderLayerContext
      constructor : (@ctx) ->
        @pathPainter  = new seen.CanvasPathPainter(@ctx)
        @ciclePainter = new seen.CanvasCirclePainter(@ctx)
        @textPainter  = new seen.CanvasTextPainter(@ctx)
        @rectPainter  = new seen.CanvasRectPainter(@ctx)
      
      path   : () -> @pathPainter
      rect   : () -> @rectPainter
      circle : () -> @ciclePainter
      text   : () -> @textPainter
    
    class seen.CanvasRenderContext extends seen.RenderContext
      constructor: (@el) ->
        super()
        @el  = seen.Util.element(@el)
        @ctx = @el.getContext('2d')
    
      layer : (layer) ->
        @layers.push {
          layer   : layer
          context : new seen.CanvasLayerRenderContext(@ctx)
        }
        return @
    
      reset : ->
        @ctx.setTransform(1, 0, 0, 1, 0, 0)
        @ctx.clearRect(0, 0, @el.width, @el.height)
    
    seen.CanvasContext = (elementId, scene) ->
      context = new seen.CanvasRenderContext(elementId)
      if scene? then context.sceneLayer(scene)
      return context
  • ¶

    Interaction

    Mouse drag and zoom


  • ¶
  • ¶

    A global window event dispatcher. Attaches listeners only if window is defined.

    seen.WindowEvents = do ->
      dispatch = seen.Events.dispatch(
        'mouseMove'
        'mouseDown'
        'mouseUp'
        'touchStart'
        'touchMove'
        'touchEnd'
        'touchCancel'
      )
    
      if window?
        window.addEventListener('mouseup', dispatch.mouseUp, true)
        window.addEventListener('mousedown', dispatch.mouseDown, true)
        window.addEventListener('mousemove', dispatch.mouseMove, true)
        window.addEventListener('touchstart', dispatch.touchStart, true)
        window.addEventListener('touchmove', dispatch.touchMove, true)
        window.addEventListener('touchend', dispatch.touchEnd, true)
        window.addEventListener('touchcancel', dispatch.touchCancel, true)
      return {on : dispatch.on}
  • ¶

    An event dispatcher for mouse and drag events on a single dom element. The available events are 'dragStart', 'drag', 'dragEnd', 'mouseMove', 'mouseDown', 'mouseUp', 'mouseWheel'

    class seen.MouseEvents
      constructor : (@el, options) ->
        seen.Util.defaults(@, options, @defaults)
    
        @el = seen.Util.element(@el)
    
        @_uid = seen.Util.uniqueId('mouser-')
    
        @dispatch = seen.Events.dispatch(
          'dragStart'
          'drag'
          'dragEnd'
          'mouseMove'
          'mouseDown'
          'mouseUp'
          'mouseWheel'
        )
        @on = @dispatch.on
    
        @_mouseDown = false
        @attach()
  • ¶

    Attaches listeners to the element

      attach : () ->
        @el.addEventListener('touchstart', @_onMouseDown)
        @el.addEventListener('mousedown', @_onMouseDown)
        @el.addEventListener('mousewheel', @_onMouseWheel)
  • ¶

    Dettaches listeners to the element

      detach : () ->
        @el.removeEventListener('touchstart', @_onMouseDown)
        @el.removeEventListener('mousedown', @_onMouseDown)
        @el.removeEventListener('mousewheel', @_onMouseWheel)
    
      _onMouseMove : (e) =>
        @dispatch.mouseMove(e)
        e.preventDefault()
        e.stopPropagation()
        if @_mouseDown then @dispatch.drag(e)
    
      _onMouseDown : (e) =>
        @_mouseDown = true
        seen.WindowEvents.on "mouseUp.#{@_uid}", @_onMouseUp
        seen.WindowEvents.on "mouseMove.#{@_uid}", @_onMouseMove
        seen.WindowEvents.on "touchEnd.#{@_uid}", @_onMouseUp
        seen.WindowEvents.on "touchCancel.#{@_uid}", @_onMouseUp
        seen.WindowEvents.on "touchMove.#{@_uid}", @_onMouseMove
        @dispatch.mouseDown(e)
        @dispatch.dragStart(e)
    
      _onMouseUp : (e) =>
        @_mouseDown = false
        seen.WindowEvents.on "mouseUp.#{@_uid}", null
        seen.WindowEvents.on "mouseMove.#{@_uid}", null
        seen.WindowEvents.on "touchEnd.#{@_uid}", null
        seen.WindowEvents.on "touchCancel.#{@_uid}", null
        seen.WindowEvents.on "touchMove.#{@_uid}", null
        @dispatch.mouseUp(e)
        @dispatch.dragEnd(e)
    
      _onMouseWheel : (e) =>
        @dispatch.mouseWheel(e)
  • ¶

    A class for computing mouse interia for interial scrolling

    class seen.InertialMouse
      @inertiaExtinction : 0.1
      @smoothingTimeout  : 300
      @inertiaMsecDelay  : 30
    
      constructor : ->
        @reset()
    
      get : ->
        scale = 1000 / seen.InertialMouse.inertiaMsecDelay
        return [@x * scale, @y * scale]
    
      reset : ->
        @xy = [0, 0]
        return @
    
      update : (xy) ->
        if @lastUpdate?
          msec = new Date().getTime() - @lastUpdate.getTime() # Time passed
          xy = xy.map (x) -> x / Math.max(msec, 1) # Pixels per milliseconds
          t = Math.min(1, msec / seen.InertialMouse.smoothingTimeout) # Interpolation based on time between measurements
          @x = t * xy[0] + (1.0 - t) * @x
          @y = t * xy[1] + (1.0 - t) * @y
        else
         [@x, @y] = xy
    
        @lastUpdate = new Date()
        return @
  • ¶

    Apply damping to slow the motion once the user has stopped dragging.

      damp : ->
        @x *= (1.0 - seen.InertialMouse.inertiaExtinction)
        @y *= (1.0 - seen.InertialMouse.inertiaExtinction)
        return @
  • ¶

    Adds simple mouse drag eventing to a DOM element. A ‘drag’ event is emitted as the user is dragging their mouse. This is the easiest way to add mouse- look or mouse-rotate to a scene.

    class seen.Drag
      defaults:
        inertia : false
    
      constructor : (@el, options) ->
        seen.Util.defaults(@, options, @defaults)
        @el = seen.Util.element(@el)
        @_uid = seen.Util.uniqueId('dragger-')
    
        @_inertiaRunning = false
        @_dragState =
          dragging : false
          origin   : null
          last     : null
          inertia  : new seen.InertialMouse()
    
        @dispatch = seen.Events.dispatch('drag', 'dragStart', 'dragEnd', 'dragEndInertia')
        @on       = @dispatch.on
    
        mouser = new seen.MouseEvents(@el)
        mouser.on "dragStart.#{@_uid}", @_onDragStart
        mouser.on "dragEnd.#{@_uid}", @_onDragEnd
        mouser.on "drag.#{@_uid}", @_onDrag
    
      _getPageCoords : (e) =>
        return if e.touches?.length > 0
          [e.touches[0].pageX, e.touches[0].pageY]
        else if e.changedTouches?.length > 0
          [e.changedTouches[0].pageX, e.changedTouches[0].pageY]
        else
          [e.pageX, e.pageY]
    
      _onDragStart : (e) =>
        @_stopInertia()
        @_dragState.dragging = true
        @_dragState.origin   = @_getPageCoords(e)
        @_dragState.last     = @_getPageCoords(e)
        @dispatch.dragStart(e)
    
      _onDragEnd : (e) =>
        @_dragState.dragging = false
    
        if @inertia
          page = @_getPageCoords(e)
          dragEvent =
            offset         : [page[0] - @_dragState.origin[0], page[1] - @_dragState.origin[1]]
            offsetRelative : [page[0] - @_dragState.last[0], page[1] - @_dragState.last[1]]
    
          @_dragState.inertia.update(dragEvent.offsetRelative)
          @_startInertia()
    
        @dispatch.dragEnd(e)
    
      _onDrag : (e) =>
        page = @_getPageCoords(e)
    
        dragEvent =
          offset         : [page[0] - @_dragState.origin[0], page[1] - @_dragState.origin[1]]
          offsetRelative : [page[0] - @_dragState.last[0], page[1]- @_dragState.last[1]]
    
        @dispatch.drag(dragEvent)
    
        if @inertia
          @_dragState.inertia.update(dragEvent.offsetRelative)
    
        @_dragState.last = page
    
      _onInertia : () =>
        return unless @_inertiaRunning
  • ¶

    Apply damping and get x,y intertia values

        intertia = @_dragState.inertia.damp().get()
    
        if Math.abs(intertia[0]) < 1 and Math.abs(intertia[1]) < 1
          @_stopInertia()
          @dispatch.dragEndInertia()
          return
    
        @dispatch.drag(
          offset         : [@_dragState.last[0] - @_dragState.origin[0], @_dragState.last[0] - @_dragState.origin[1]]
          offsetRelative : intertia
        )
        @_dragState.last = [@_dragState.last[0] + intertia[0], @_dragState.last[1] + intertia[1]]
    
        @_startInertia()
    
      _startInertia : =>
        @_inertiaRunning = true
        setTimeout(@_onInertia, seen.InertialMouse.inertiaMsecDelay)
    
      _stopInertia : =>
        @_dragState.inertia.reset()
        @_inertiaRunning = false
  • ¶

    Adds simple mouse wheel eventing to a DOM element. A ‘zoom’ event is emitted as the user is scrolls their mouse wheel.

    class seen.Zoom
      defaults :
        speed : 0.25
    
      constructor : (@el,  options) ->
        seen.Util.defaults(@, options, @defaults)
        @el       = seen.Util.element(@el)
        @_uid     = seen.Util.uniqueId('zoomer-')
        @dispatch = seen.Events.dispatch('zoom')
        @on       = @dispatch.on
    
        mouser    = new seen.MouseEvents(@el)
        mouser.on "mouseWheel.#{@_uid}", @_onMouseWheel
    
      _onMouseWheel : (e) =>
  • ¶

    This prevents the page from scrolling when we mousewheel the element

        e.preventDefault()
    
        sign       = e.wheelDelta / Math.abs(e.wheelDelta)
        zoomFactor = Math.abs(e.wheelDelta) / 120 * @speed
        zoom       = Math.pow(2, sign*zoomFactor)
    
        @dispatch.zoom({zoom})
  • ¶

    Surfaces and Shapes


  • ¶
  • ¶

    A Surface is a defined as a planar object in 3D space. These paths don’t necessarily need to be convex, but they should be non-degenerate. This library does not support shapes with holes.

    class seen.Surface
  • ¶

    When ‘false’ this will override backface culling, which is useful if your material is transparent. See comment in seen.Scene.

      cullBackfaces  : true
  • ¶

    Fill and stroke may be Material objects, which define the color and finish of the object and are rendered using the scene’s shader.

      fillMaterial   : new seen.Material(seen.C.gray)
      strokeMaterial : null
    
      constructor : (@points, @painter = seen.Painters.path) ->
  • ¶

    We store a unique id for every surface so we can look them up quickly with the renderModel cache.

        @id = 's' + seen.Util.uniqueId()
    
      fill : (fill) ->
        @fillMaterial = seen.Material.create(fill)
        return @
    
      stroke : (stroke) ->
        @strokeMaterial = seen.Material.create(stroke)
        return @
  • ¶

    A Shape contains a collection of surface. They may create a closed 3D shape, but not necessarily. For example, a cube is a closed shape, but a patch is not.

    class seen.Shape extends seen.Transformable
      constructor : (@type, @surfaces) ->
        super()
  • ¶

    Visit each surface

      eachSurface: (f) ->
        @surfaces.forEach(f)
        return @
  • ¶

    Apply the supplied fill Material to each surface

      fill : (fill) ->
        @eachSurface (s) -> s.fill(fill)
        return @
  • ¶

    Apply the supplied stroke Material to each surface

      stroke : (stroke) ->
        @eachSurface (s) -> s.stroke(stroke)
        return @
  • ¶

    Models


  • ¶
  • ¶

    The object model class. It stores Shapes, Lights, and other Models as well as a transformation matrix.

    Notably, models are hierarchical, like a tree. This means you can isolate the transformation of groups of shapes in the scene, as well as create chains of transformations for creating, for example, articulated skeletons.

    class seen.Model extends seen.Transformable
      constructor: () ->
        super()
        @children = []
        @lights   = []
  • ¶

    Add a Shape, Light, and other Model as a child of this Model Any number of children can by supplied as arguments.

      add: (childs...) ->
        for child in childs
          if child instanceof seen.Shape or child instanceof seen.Model
            @children.push child
          else if child instanceof seen.Light
            @lights.push child
        return @
  • ¶

    Remove a shape, model, or light from the model. NOTE: the scene may still contain a renderModel in its cache. If you are adding and removing many items, consider calling .flush() on the scene to flush its renderModel cache.

      remove : (childs...) ->
        for child in childs
          while (i = @children.indexOf(child)) >= 0
            @children.splice(i,1)
          while (i = @lights.indexOf(child)) >= 0
            @lights.splice(i,1)
  • ¶

    Create a new child model and return it.

      append: () ->
        model = new seen.Model
        @add model
        return model
  • ¶

    Visit each Shape in this Model and all recursive child Models.

      eachShape: (f) ->
        for child in @children
          if child instanceof seen.Shape
            f.call(@, child)
          if child instanceof seen.Model
            child.eachShape(f)
  • ¶

    Visit each Light and Shape, accumulating the recursive transformation matrices along the way. The light callback will be called with each light and its accumulated transform and it should return a LightModel. Each shape callback with be called with each shape and its accumulated transform as well as the list of light models that apply to that shape.

      eachRenderable : (lightFn, shapeFn) ->
        @_eachRenderable(lightFn, shapeFn, [], @m)
    
      _eachRenderable : (lightFn, shapeFn, lightModels, transform) ->
        if @lights.length > 0 then lightModels = lightModels.slice()
        for light in @lights
          continue unless light.enabled
          lightModels.push lightFn.call(@, light, light.m.copy().multiply(transform))
    
        for child in @children
          if child instanceof seen.Shape
            shapeFn.call(@, child, lightModels, child.m.copy().multiply(transform))
          if child instanceof seen.Model
            child._eachRenderable(lightFn, shapeFn, lightModels, child.m.copy().multiply(transform))
    
    seen.Models = {
  • ¶

    The default model contains standard Hollywood-style 3-part lighting

      default : ->
        model = new seen.Model()
  • ¶

    Key light

        model.add seen.Lights.directional
          normal    : seen.P(-1, 1, 1).normalize()
          color     : seen.Colors.hsl(0.1, 0.3, 0.7)
          intensity : 0.004
  • ¶

    Back light

        model.add seen.Lights.directional
          normal    : seen.P(1, 1, -1).normalize()
          intensity : 0.003
  • ¶

    Fill light

        model.add seen.Lights.ambient
          intensity : 0.0015
    
        return model
    }
  • ¶

    Animator


  • ¶
  • ¶

    Polyfill requestAnimationFrame

    if window?
      requestAnimationFrame =
          window.requestAnimationFrame ?
          window.mozRequestAnimationFrame ?
          window.webkitRequestAnimationFrame ?
          window.msRequestAnimationFrame
    
    DEFAULT_FRAME_DELAY = 30 # msec
  • ¶

    The animator class is useful for creating an animation loop. We supply pre and post events for apply animation changes between frames.

    class seen.Animator
      constructor : () ->
        @dispatch   = seen.Events.dispatch('beforeFrame', 'afterFrame', 'frame')
        @on         = @dispatch.on
        @timestamp  = 0
        @_running   = false
        @frameDelay = null
  • ¶

    Start the animation loop.

      start : ->
        @_running   = true
    
        if @frameDelay?
          @_lastTime = new Date().valueOf()
          @_delayCompensation = 0
    
        @animateFrame()
        return @
  • ¶

    Stop the animation loop.

      stop : ->
        @_running = false
        return @
  • ¶

    Use requestAnimationFrame if available and we have no explicit frameDelay. Otherwise, use a delay-compensated timeout.

      animateFrame : ->
        if requestAnimationFrame? and not @frameDelay?
          requestAnimationFrame(@frame)
        else
  • ¶

    Perform frame delay compensation to make sure each frame is rendered at the right time. This makes some animations more consistent

          delta = new Date().valueOf() - @_lastTime
          @_lastTime += delta
          @_delayCompensation += delta
    
          frameDelay = @frameDelay ? DEFAULT_FRAME_DELAY
          setTimeout(@frame, frameDelay - @_delayCompensation)
        return @
  • ¶

    The main animation frame method

      frame : (t) =>
        return unless @_running
  • ¶

    create timestamp param even if requestAnimationFrame isn’t available

        @_timestamp    = t ? (@_timestamp + (@_msecDelay ? DEFAULT_FRAME_DELAY))
        deltaTimestamp = if @_lastTimestamp? then @_timestamp - @_lastTimestamp else @_timestamp
    
        @dispatch.beforeFrame(@_timestamp, deltaTimestamp)
        @dispatch.frame(@_timestamp, deltaTimestamp)
        @dispatch.afterFrame(@_timestamp, deltaTimestamp)
    
        @_lastTimestamp = @_timestamp
    
        @animateFrame()
        return @
  • ¶

    Add a callback that will be invoked before the frame

      onBefore : (handler) ->
        @on "beforeFrame.#{seen.Util.uniqueId('animator-')}", handler
        return @
  • ¶

    Add a callback that will be invoked after the frame

      onAfter : (handler) ->
        @on "afterFrame.#{seen.Util.uniqueId('animator-')}", handler
        return @
  • ¶

    Add a frame callback

      onFrame : (handler) ->
        @on "frame.#{seen.Util.uniqueId('animator-')}", handler
        return @
  • ¶

    A seen.Animator for rendering the seen.Context

    class seen.RenderAnimator extends seen.Animator
      constructor : (context) ->
        super
        @onFrame(context.render)
  • ¶

    A transition object to manage to animation of shapes

    class seen.Transition
      defaults :
        duration : 100 # The duration of this transition in msec
    
      constructor : (options = {}) ->
        seen.Util.defaults(@, options, @defaults)
    
      update : (t) ->
  • ¶

    Setup the first frame before the tick increment

        if not @t?
          @firstFrame()
          @startT = t
  • ¶

    Execute a tick and draw a frame

        @t = t
        @tFrac = (@t - @startT) / @duration
        @frame()
  • ¶

    Cleanup or update on last frame after tick

        if (@tFrac >= 1.0)
          @lastFrame()
          return false
    
        return true
    
      firstFrame : ->
      frame      : ->
      lastFrame  : ->
  • ¶

    A seen.Animator for updating seen.Transtions. We include keyframing to make sure we wait for one transition to finish before starting the next one.

    class seen.TransitionAnimator extends seen.Animator
      constructor : ->
        super
        @queue       = []
        @transitions = []
        @onFrame(@update)
  • ¶

    Adds a transition object to the current set of transitions. Note that transitions will not start until they have been enqueued by invoking keyframe() on this object.

      add : (txn) ->
        @transitions.push txn
  • ¶

    Enqueues the current set of transitions into the keyframe queue and sets up a new set of transitions.

      keyframe : ->
        @queue.push @transitions
        @transitions = []
  • ¶

    When this animator updates, it invokes update() on all of the currently animating transitions. If any of the current transitions are not done, we re-enqueue them at the front. If all transitions are complete, we will start animating the next set of transitions from the keyframe queue on the next update.

      update : (t) =>
        return unless @queue.length
        transitions = @queue.shift()
        transitions = transitions.filter (transition) -> transition.update(t)
        if transitions.length then @queue.unshift(transitions)
  • ¶

    Shapes

    Shape primitives and shape-making methods


  • ¶
  • ¶

    Map to points in the surfaces of a tetrahedron

    TETRAHEDRON_COORDINATE_MAP = [
      [0, 2, 1]
      [0, 1, 3]
      [3, 2, 0]
      [1, 2, 3]
    ]
  • ¶

    Map to points in the surfaces of a cube

    CUBE_COORDINATE_MAP = [
      [0, 1, 3, 2] # left
      [5, 4, 6, 7] # right
      [1, 0, 4, 5] # bottom
      [2, 3, 7, 6] # top
      [3, 1, 5, 7] # front
      [0, 2, 6, 4] # back
    ]
  • ¶

    Map to points in the surfaces of a rectangular pyramid

    PYRAMID_COORDINATE_MAP = [
      [1, 0, 2, 3] # bottom
      [0, 1, 4] # left
      [2, 0, 4] # rear
      [3, 2, 4] # right
      [1, 3, 4] # front
    ]
  • ¶

    Altitude of eqiulateral triangle for computing triangular patch size

    EQUILATERAL_TRIANGLE_ALTITUDE = Math.sqrt(3.0) / 2.0
  • ¶

    Points array of an icosahedron

    ICOS_X = 0.525731112119133606
    ICOS_Z = 0.850650808352039932
    ICOSAHEDRON_POINTS = [
      seen.P(-ICOS_X, 0.0,     -ICOS_Z)
      seen.P(ICOS_X,  0.0,     -ICOS_Z)
      seen.P(-ICOS_X, 0.0,     ICOS_Z)
      seen.P(ICOS_X,  0.0,     ICOS_Z)
      seen.P(0.0,     ICOS_Z,  -ICOS_X)
      seen.P(0.0,     ICOS_Z,  ICOS_X)
      seen.P(0.0,     -ICOS_Z, -ICOS_X)
      seen.P(0.0,     -ICOS_Z, ICOS_X)
      seen.P(ICOS_Z,  ICOS_X,  0.0)
      seen.P(-ICOS_Z, ICOS_X,  0.0)
      seen.P(ICOS_Z,  -ICOS_X, 0.0)
      seen.P(-ICOS_Z, -ICOS_X, 0.0)
    ]
  • ¶

    Map to points in the surfaces of an icosahedron

    ICOSAHEDRON_COORDINATE_MAP = [
      [0, 4, 1]
      [0, 9, 4]
      [9, 5, 4]
      [4, 5, 8]
      [4, 8, 1]
      [8, 10, 1]
      [8, 3, 10]
      [5, 3, 8]
      [5, 2, 3]
      [2, 7, 3]
      [7, 10, 3]
      [7, 6, 10]
      [7, 11, 6]
      [11, 0, 6]
      [0, 1, 6]
      [6, 1, 10]
      [9, 0, 11]
      [9, 11, 2]
      [9, 2, 5]
      [7, 2, 11]
    ]
    
    seen.Shapes = {
  • ¶

    Returns a 2x2x2 cube, centered on the origin.

      cube: =>
        points = [
          seen.P(-1, -1, -1)
          seen.P(-1, -1,  1)
          seen.P(-1,  1, -1)
          seen.P(-1,  1,  1)
          seen.P( 1, -1, -1)
          seen.P( 1, -1,  1)
          seen.P( 1,  1, -1)
          seen.P( 1,  1,  1)
        ]
    
        return new seen.Shape('cube', seen.Shapes.mapPointsToSurfaces(points, CUBE_COORDINATE_MAP))
  • ¶

    Returns a 1x1x1 cube from the origin to [1, 1, 1].

      unitcube: =>
        points = [
          seen.P(0, 0, 0)
          seen.P(0, 0, 1)
          seen.P(0, 1, 0)
          seen.P(0, 1, 1)
          seen.P(1, 0, 0)
          seen.P(1, 0, 1)
          seen.P(1, 1, 0)
          seen.P(1, 1, 1)
        ]
    
        return new seen.Shape('unitcube', seen.Shapes.mapPointsToSurfaces(points, CUBE_COORDINATE_MAP))
  • ¶

    Returns an axis-aligned 3D rectangle whose boundaries are defined by the two supplied points.

      rectangle : (point1, point2) =>
        compose = (x, y, z) ->
          return seen.P(
            x(point1.x, point2.x)
            y(point1.y, point2.y)
            z(point1.z, point2.z)
          )
    
        points = [
          compose(Math.min, Math.min, Math.min)
          compose(Math.min, Math.min, Math.max)
          compose(Math.min, Math.max, Math.min)
          compose(Math.min, Math.max, Math.max)
          compose(Math.max, Math.min, Math.min)
          compose(Math.max, Math.min, Math.max)
          compose(Math.max, Math.max, Math.min)
          compose(Math.max, Math.max, Math.max)
        ]
    
        return new seen.Shape('rect', seen.Shapes.mapPointsToSurfaces(points, CUBE_COORDINATE_MAP))
  • ¶

    Returns a square pyramid inside a unit cube

      pyramid : =>
        points = [
          seen.P(0, 0, 0)
          seen.P(0, 0, 1)
          seen.P(1, 0, 0)
          seen.P(1, 0, 1)
          seen.P(0.5, 1, 0.5)
        ]
    
        return new seen.Shape('pyramid', seen.Shapes.mapPointsToSurfaces(points, PYRAMID_COORDINATE_MAP))
  • ¶

    Returns a tetrahedron that fits inside a 2x2x2 cube.

      tetrahedron: =>
        points = [
          seen.P( 1,  1,  1)
          seen.P(-1, -1,  1)
          seen.P(-1,  1, -1)
          seen.P( 1, -1, -1)]
    
        return new seen.Shape('tetrahedron', seen.Shapes.mapPointsToSurfaces(points, TETRAHEDRON_COORDINATE_MAP))
  • ¶

    Returns an icosahedron that fits within a 2x2x2 cube, centered on the origin.

      icosahedron : ->
        return new seen.Shape('icosahedron', seen.Shapes.mapPointsToSurfaces(ICOSAHEDRON_POINTS, ICOSAHEDRON_COORDINATE_MAP))
  • ¶

    Returns a sub-divided icosahedron, which approximates a sphere with triangles of equal size.

      sphere : (subdivisions = 2) ->
        triangles = ICOSAHEDRON_COORDINATE_MAP.map (coords) -> coords.map (c) -> ICOSAHEDRON_POINTS[c]
        for i in [0...subdivisions]
          triangles = seen.Shapes._subdivideTriangles(triangles)
        return new seen.Shape('sphere', triangles.map (triangle) -> new seen.Surface(triangle.map (v) -> v.copy()))
    
    
  • ¶

    Returns a cylinder whose main axis is aligned from point1 to point2

      pipe : (point1, point2, radius = 1, segments = 8) ->
  • ¶

    Compute a normal perpendicular to the axis point1->point2 and define the rotations about the axis as a quaternion

        axis  = point2.copy().subtract(point1)
        perp  = axis.perpendicular().multiply(radius)
        theta = -Math.PI * 2.0 / segments
        quat  = seen.Quaternion.pointAngle(axis.copy().normalize(), theta).toMatrix()
  • ¶

    Apply the quaternion rotations to create one face

        points = [0...segments].map (i) ->
          p = point1.copy().add(perp)
          perp.transform(quat)
          return p
    
        return seen.Shapes.extrude(points, axis)
  • ¶

    Returns a planar triangular patch. The supplied arguments determine the number of triangle in the patch.

      patch: (nx = 20, ny = 20) ->
        nx = Math.round(nx)
        ny = Math.round(ny)
        surfaces = []
        for x in [0...nx]
          column = []
          for y in [0...ny]
            pts0 = [
              seen.P(x, y)
              seen.P(x + 1, y - 0.5)
              seen.P(x + 1, y + 0.5)
            ]
            pts1 = [
              seen.P(x, y)
              seen.P(x + 1, y + 0.5)
              seen.P(x, y + 1)
            ]
    
            for pts in [pts0, pts1]
              for p in pts
                p.x *= EQUILATERAL_TRIANGLE_ALTITUDE
                p.y += if x % 2 is 0 then 0.5 else 0
              column.push pts
    
          if x % 2 isnt 0
            for p in column[0]
              p.y += ny
            column.push column.shift()
          surfaces = surfaces.concat(column)
    
        return new seen.Shape('patch', surfaces.map((s) -> new seen.Surface(s)))
    
    
  • ¶

    Return a text surface that can render 3D text using an affine transform estimate of the projection

      text : (text, surfaceOptions = {}) ->
        surface = new seen.Surface(seen.Affine.ORTHONORMAL_BASIS(), seen.Painters.text)
        surface.text = text
        for key, val of surfaceOptions
          surface[key] = val
        return new seen.Shape('text', [surface])
  • ¶

    Returns a shape that is an extrusion of the supplied points into the z axis.

      extrude : (points, offset) ->
        surfaces = []
        front = new seen.Surface (p.copy() for p in points)
        back  = new seen.Surface (p.add(offset) for p in points)
    
        for i in [1...points.length]
          surfaces.push new seen.Surface [
            front.points[i - 1].copy()
            back.points[i - 1].copy()
            back.points[i].copy()
            front.points[i].copy()
          ]
    
        len = points.length
        surfaces.push new seen.Surface [
          front.points[len - 1].copy()
          back.points[len - 1].copy()
          back.points[0].copy()
          front.points[0].copy()
        ]
    
        back.points.reverse()
        surfaces.push front
        surfaces.push back
        return new seen.Shape('extrusion', surfaces)
  • ¶

    Returns an extruded block arrow shape.

      arrow : (thickness = 1, tailLength = 1, tailWidth = 1, headLength = 1, headPointiness = 0) ->
        htw = tailWidth/2
        points = [
          seen.P(0, 0, 0)
          seen.P(headLength + headPointiness, 1, 0)
          seen.P(headLength, htw, 0)
          seen.P(headLength + tailLength, htw, 0)
          seen.P(headLength + tailLength, -htw, 0)
          seen.P(headLength, -htw, 0)
          seen.P(headLength + headPointiness, -1, 0)
        ]
        return seen.Shapes.extrude(points, seen.P(0,0,thickness))
  • ¶

    Returns a shape with a single surface using the supplied points array

      path : (points) ->
        return new seen.Shape('path', [new seen.Surface(points)])
  • ¶

    Accepts a 2-dimensional array of tuples, returns a shape where the tuples represent points of a planar surface.

      custom : (s) ->
        surfaces = []
        for f in s.surfaces
          surfaces.push new seen.Surface((seen.P(p...) for p in f))
        return new seen.Shape('custom', surfaces)
  • ¶

    Joins the points into surfaces using the coordinate map, which is an 2-dimensional array of index integers.

      mapPointsToSurfaces : (points, coordinateMap) ->
        surfaces = []
        for coords in coordinateMap
          spts = (points[c].copy() for c in coords)
          surfaces.push(new seen.Surface(spts))
        return surfaces
  • ¶

    Accepts an array of 3-tuples and returns an array of 3-tuples representing the triangular subdivision of the surface.

      _subdivideTriangles : (triangles) ->
        newTriangles = []
        for tri in triangles
          v01 = tri[0].copy().add(tri[1]).normalize()
          v12 = tri[1].copy().add(tri[2]).normalize()
          v20 = tri[2].copy().add(tri[0]).normalize()
          newTriangles.push [tri[0], v01, v20]
          newTriangles.push [tri[1], v12, v01]
          newTriangles.push [tri[2], v20, v12]
          newTriangles.push [v01,    v12, v20]
        return newTriangles
    }
    
    
    
    class seen.MocapModel
      constructor : (@model, @frames, @frameDelay) ->
    
      applyFrameTransforms : (frameIndex) ->
        frame = @frames[frameIndex]
        for transform in frame
          transform.shape.reset().transform(transform.transform)
        return (frameIndex + 1) % @frames.length
    
    
    class seen.MocapAnimator extends seen.Animator
      constructor : (@mocap) ->
        super
        @frameIndex = 0
        @frameDelay = @mocap.frameDelay
        @onFrame(@renderFrame)
    
      renderFrame : =>
        @frameIndex = @mocap.applyFrameTransforms(@frameIndex)
    
    
    class seen.Mocap
      @DEFAULT_SHAPE_FACTORY : (joint, endpoint) ->
        return seen.Shapes.pipe(seen.P(), endpoint)
    
      @parse : (source) ->
        return new seen.Mocap(seen.BvhParser.parse(source))
    
      constructor : (@bvh) ->
    
      createMocapModel : (shapeFactory = seen.Mocap.DEFAULT_SHAPE_FACTORY) ->
        model = new seen.Model()
        joints = []
        @_attachJoint(model, @bvh.root, joints, shapeFactory)
        frames = @bvh.motion.frames.map (frame) => @_generateFrameTransforms(frame, joints)
        return new seen.MocapModel(model, frames, @bvh.motion.frameTime * 1000)
    
      _generateFrameTransforms : (frame, joints) ->
        fi = 0
        transforms = joints.map (joint) =>
  • ¶

    Apply channel actions in reverse order

          m  = seen.M()
          ai = joint.channels.length
          while ai > 0
            ai -= 1
            @_applyChannelTransform(joint.channels[ai], m, frame[fi + ai])
          fi += joint.channels.length
  • ¶

    Include offset as final tranform

          m.multiply(joint.offset)
    
          return {
            shape     : joint.shape
            transform : m
          }
    
        return transforms
    
      _applyChannelTransform : (channel, m, v) ->
        switch channel
          when 'Xposition' then m.translate(v, 0, 0)
          when 'Yposition' then m.translate(0, v, 0)
          when 'Zposition' then m.translate(0, 0, v)
          when 'Xrotation' then m.rotx(v * Math.PI / 180.0)
          when 'Yrotation' then m.roty(v * Math.PI / 180.0)
          when 'Zrotation' then m.rotz(v * Math.PI / 180.0)
        return m
    
      _attachJoint : (model, joint, joints, shapeFactory) ->
  • ¶

    Save joint offset

        offset = seen.M().translate(
          joint.offset?.x
          joint.offset?.y
          joint.offset?.z
        )
        model.transform(offset)
  • ¶

    Create channel actions

        if joint.channels?
          joints.push {
            shape    : model
            offset   : offset
            channels : joint.channels
          }
    
        if joint.joints?
  • ¶

    Append a model to store the child shapes

          childShapes = model.append()
    
          for child in joint.joints
  • ¶

    Generate the child shape with the supplied shape factory

            p = seen.P(child.offset?.x, child.offset?.y, child.offset?.z)
            childShapes.add(shapeFactory(joint, p))
  • ¶

    Recurse with a new model for any child joints

            if child.type is 'JOINT' then @_attachJoint(childShapes.append(), child, joints, shapeFactory)
        return
  • ¶

    Parser for Wavefront .obj files

    Note: Wavefront .obj array indicies are 1-based.

    class seen.ObjParser
      constructor : () ->
        @vertices = []
        @faces    = []
        @commands =
          v : (data) => @vertices.push data.map (d) -> parseFloat(d)
          f : (data) => @faces.push data.map (d) -> parseInt(d)
    
      parse : (contents) ->
        for line in contents.split(/[\r\n]+/)
          data = line.trim().split(/[ ]+/)
    
          continue if data.length < 2 # Check line parsing
    
          command = data.slice(0,1)[0]
          data    = data.slice(1)
          
          if command.charAt(0) is '#' # Check for comments
            continue
          if not @commands[command]? # Check that we know how the handle this command
            console.log "OBJ Parser: Skipping unknown command '#{command}'"
            continue
    
          @commands[command](data) # Execute command
    
      mapFacePoints : (faceMap) ->
        @faces.map (face) =>
          points = face.map (v) => seen.P(@vertices[v - 1]...)
          return faceMap.call(@, points)
  • ¶

    This method accepts Wavefront .obj file content and returns a Shape object.

    seen.Shapes.obj = (objContents, cullBackfaces = true) ->
      parser = new seen.ObjParser()
      parser.parse(objContents)
      return new seen.Shape('obj', parser.mapFacePoints((points) ->
        surface = new seen.Surface points
        surface.cullBackfaces = cullBackfaces
        return surface
      ))
    
    
    
  • ¶

    Camera

    Projections, Viewports, and Cameras


  • ¶
  • ¶

    These projection methods return a 3D to 2D Matrix transformation. Each projection assumes the camera is located at (0,0,0).

    seen.Projections = {
  • ¶

    Creates a perspective projection matrix

      perspectiveFov : (fovyInDegrees = 50, front = 1) ->
        tan = front * Math.tan(fovyInDegrees * Math.PI / 360.0)
        return seen.Projections.perspective(-tan, tan, -tan, tan, front, 2*front)
  • ¶

    Creates a perspective projection matrix with the supplied frustrum

      perspective : (left=-1, right=1, bottom=-1, top=1, near=1, far=100) ->
        near2 = 2 * near
        dx    = right - left
        dy    = top - bottom
        dz    = far - near
    
        m = new Array(16)
        m[0]  = near2 / dx
        m[1]  = 0.0
        m[2]  = (right + left) / dx
        m[3]  = 0.0
    
        m[4]  = 0.0
        m[5]  = near2 / dy
        m[6]  = (top + bottom) / dy
        m[7]  = 0.0
    
        m[8]  = 0.0
        m[9]  = 0.0
        m[10] = -(far + near) / dz
        m[11] = -(far * near2) / dz
    
        m[12] = 0.0
        m[13] = 0.0
        m[14] = -1.0
        m[15] = 0.0
        return seen.M(m)
  • ¶

    Creates a orthographic projection matrix with the supplied frustrum

      ortho : (left=-1, right=1, bottom=-1, top=1, near=1, far=100) ->
        near2 = 2 * near
        dx    = right - left
        dy    = top - bottom
        dz    = far - near
    
        m = new Array(16)
        m[0]  = 2 / dx
        m[1]  = 0.0
        m[2]  = 0.0
        m[3]  = (right + left) / dx
    
        m[4]  = 0.0
        m[5]  = 2 / dy
        m[6]  = 0.0
        m[7]  = -(top + bottom) / dy
    
        m[8]  = 0.0
        m[9]  = 0.0
        m[10] = -2 / dz
        m[11] = -(far + near) / dz
    
        m[12] = 0.0
        m[13] = 0.0
        m[14] = 0.0
        m[15] = 1.0
        return seen.M(m)
    }
    
    seen.Viewports = {
  • ¶

    Create a viewport where the scene’s origin is centered in the view

      center : (width = 500, height = 500, x = 0, y = 0) ->
        prescale = seen.M()
          .translate(-x, -y, -height)
          .scale(1/width, 1/height, 1/height)
    
        postscale = seen.M()
          .scale(width, -height, height)
          .translate(x + width/2, y + height/2, height)
        return {prescale, postscale}
  • ¶

    Create a view port where the scene’s origin is aligned with the origin ([0, 0]) of the view

      origin : (width = 500, height = 500, x = 0, y = 0) ->
        prescale = seen.M()
          .translate(-x, -y, -1)
          .scale(1/width, 1/height, 1/height)
    
        postscale = seen.M()
          .scale(width, -height, height)
          .translate(x, y)
        return {prescale, postscale}
    }
  • ¶

    The Camera model contains all three major components of the 3D to 2D tranformation.

    First, we transform object from world-space (the same space that the coordinates of surface points are in after all their transforms are applied) to camera space. Typically, this will place all viewable objects into a cube with coordinates: x = -1 to 1, y = -1 to 1, z = 1 to 2

    Second, we apply the projection trasform to create perspective parallax and what not.

    Finally, we rescale to the viewport size.

    These three steps allow us to easily create shapes whose coordinates match up to screen coordinates in the z = 0 plane.

    class seen.Camera extends seen.Transformable
      defaults :
        projection : seen.Projections.perspective()
    
      constructor : (options) ->
        seen.Util.defaults(@, options, @defaults)
        super
  • ¶

    Scene


  • ¶
  • ¶

    A Scene is the main object for a view of a scene.

    class seen.Scene
      defaults: ->
  • ¶

    The root model for the scene, which contains Shapes, Lights, and other Models

        model            : new seen.Model()
  • ¶

    The Camera, which defines the projection transformation. The default projection is perspective.

        camera           : new seen.Camera()
  • ¶

    The Viewport, which defines the projection from shape-space to projection-space then to screen-space. The default viewport is on a space from (0,0,0) to (1,1,1). To map more naturally to pixels, create a viewport with the same width/height as the DOM element.

        viewport         : seen.Viewports.origin(1,1)
  • ¶

    The scene’s shader determines which lighting model is used.

        shader           : seen.Shaders.phong()
  • ¶

    The cullBackfaces boolean can be used to turn off backface-culling for the whole scene. Beware, turning this off can slow down a scene’s rendering by a factor of 2. You can also turn off backface-culling for individual surfaces with a boolean on those objects.

        cullBackfaces    : true
  • ¶

    The fractionalPoints boolean determines if we round the surface coordinates to the nearest integer. Rounding the coordinates before display speeds up path drawing especially when using an SVG context since it cuts down on the length of path data. Anecdotally, my speedup on a complex demo scene was 10 FPS. However, it does introduce a slight jittering effect when animating.

        fractionalPoints : false
  • ¶

    The cache boolean (default : true) enables a simple cache for renderModels, which are generated for each surface in the scene. The cache is a simple Object keyed by the surface’s unique id. The cache has no eviction policy. To flush the cache, call .flushCache()

        cache            : true
    
      constructor: (options) ->
        seen.Util.defaults(@, options, @defaults())
        @_renderModelCache = {}
  • ¶

    The primary method that produces the render models, which are then used by the RenderContext to paint the scene.

      render : () =>
  • ¶

    Compute the projection matrix including the viewport and camera transformation matrices.

        projection = @camera.m.copy()
          .multiply(@viewport.prescale)
          .multiply(@camera.projection)
        viewport   = @viewport.postscale
    
        renderModels = []
        @model.eachRenderable(
          (light, transform) ->
  • ¶

    Compute light model data.

            new seen.LightRenderModel(light, transform)
    
          (shape, lights, transform) =>
            for surface in shape.surfaces
  • ¶

    Compute transformed and projected geometry.

              renderModel = @_renderSurface(surface, transform, projection, viewport)
  • ¶

    Test projected normal’s z-coordinate for culling (if enabled).

              if (not @cullBackfaces or not surface.cullBackfaces or renderModel.projected.normal.z < 0) and renderModel.inFrustrum
  • ¶

    Render fill and stroke using material and shader.

                renderModel.fill   = surface.fillMaterial?.render(lights, @shader, renderModel.transformed)
                renderModel.stroke = surface.strokeMaterial?.render(lights, @shader, renderModel.transformed)
  • ¶

    Round coordinates (if enabled)

                if @fractionalPoints isnt true
                  p.round() for p in renderModel.projected.points
    
                renderModels.push renderModel
        )
  • ¶

    Sort render models by projected z coordinate. This ensures that the surfaces farthest from the eye are painted first. (Painter’s Algorithm)

        renderModels.sort (a, b) ->
          return  b.projected.barycenter.z - a.projected.barycenter.z
    
        return renderModels
  • ¶

    Get or create the rendermodel for the given surface. If @cache is true, we cache these models to reduce object creation and recomputation.

      _renderSurface : (surface, transform, projection, viewport) ->
        if not @cache
          return new seen.RenderModel(surface, transform, projection, viewport)
    
        renderModel = @_renderModelCache[surface.id]
        if not renderModel?
          renderModel = @_renderModelCache[surface.id] = new seen.RenderModel(surface, transform, projection, viewport)
        else
          renderModel.update(transform, projection, viewport)
        return renderModel
  • ¶

    Removes all elements from the cache. This may be necessary if you add and remove many shapes from the scene’s models since this cache has no eviction policy.

      flushCache : () =>
        @_renderModelCache = {}
  • ¶

    Adapted from https://github.com/josephg/noisejs/blob/master/perlin.js

  • ¶

    This code was placed in the public domain by its original author, Stefan Gustavson. You may use it as you see fit, but attribution is appreciated.

    class seen.Grad
      constructor : (@x, @y, @z) ->
      dot : (x, y, z) -> @x*x + @y*y + @z*z
    
    grad3 = [
      new seen.Grad( 1, 1, 0)
      new seen.Grad(-1, 1, 0)
      new seen.Grad( 1,-1, 0)
      new seen.Grad(-1,-1, 0)
      new seen.Grad( 1, 0, 1)
      new seen.Grad(-1, 0, 1)
      new seen.Grad( 1, 0,-1)
      new seen.Grad(-1, 0,-1)
      new seen.Grad( 0, 1, 1)
      new seen.Grad( 0,-1, 1)
      new seen.Grad( 0, 1,-1)
      new seen.Grad( 0,-1,-1)
    ]
  • ¶

    To remove the need for index wrapping, double the permutation table length

    SIMPLEX_PERMUTATIONS_TABLE = [151,160,137,91,90,15,
      131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
      190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
      88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,
      77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
      102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,
      135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,
      5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
      223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,
      129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,
      251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,
      49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,
      138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]
    
    F3 = 1 / 3
    G3 = 1 / 6
    
    class seen.Simplex3D
      constructor : (seed = 0) ->
        @perm  = new Array(512)
        @gradP = new Array(512)
        @seed(seed)
  • ¶

    This isn’t a very good seeding function, but it works ok. It supports 2^16 different seed values. Write something better if you need more seeds.

      seed : (seed) ->
  • ¶

    Scale the seed out

        if(seed > 0 && seed < 1)
          seed *= 65536
    
        seed = Math.floor(seed)
        if(seed < 256)
          seed |= seed << 8
    
        for i in [0...256]
          v = 0
          if (i & 1)
            v = SIMPLEX_PERMUTATIONS_TABLE[i] ^ (seed & 255)
          else
            v = SIMPLEX_PERMUTATIONS_TABLE[i] ^ ((seed>>8) & 255)
    
          @perm[i]  = @perm[i + 256]  = v
          @gradP[i] = @gradP[i + 256] = grad3[v % 12]
    
      noise : (x, y, z) ->
  • ¶

    Skew the input space to determine which simplex cell we’re in

        s = (x + y + z)*F3 # Hairy factor for 2D
        i = Math.floor(x + s)
        j = Math.floor(y + s)
        k = Math.floor(z + s)
    
        t  = (i + j + k) * G3
        x0 = x - i + t # The x,y distances from the cell origin, unskewed.
        y0 = y - j + t
        z0 = z - k + t
  • ¶

    For the 3D case, the simplex shape is a slightly irregular tetrahedron. Determine which simplex we are in. Offsets for second corner of simplex in (i,j,k) coords Offsets for third corner of simplex in (i,j,k) coords

        if(x0 >= y0)
          if(y0 >= z0)
            i1=1; j1=0; k1=0; i2=1; j2=1; k2=0;
          else if(x0 >= z0)
            i1=1; j1=0; k1=0; i2=1; j2=0; k2=1;
          else
            i1=0; j1=0; k1=1; i2=1; j2=0; k2=1;
        else
          if(y0 < z0)
            i1=0; j1=0; k1=1; i2=0; j2=1; k2=1;
          else if(x0 < z0)
            i1=0; j1=1; k1=0; i2=0; j2=1; k2=1;
          else
            i1=0; j1=1; k1=0; i2=1; j2=1; k2=0;
  • ¶

    A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z), a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where c = 1/6.

        x1 = x0 - i1 + G3 # Offsets for second corner
        y1 = y0 - j1 + G3
        z1 = z0 - k1 + G3
    
        x2 = x0 - i2 + 2 * G3 # Offsets for third corner
        y2 = y0 - j2 + 2 * G3
        z2 = z0 - k2 + 2 * G3
    
        x3 = x0 - 1 + 3 * G3 # Offsets for fourth corner
        y3 = y0 - 1 + 3 * G3
        z3 = z0 - 1 + 3 * G3
  • ¶

    Work out the hashed gradient indices of the four simplex corners

        i &= 0xFF
        j &= 0xFF
        k &= 0xFF
        gi0 = @gradP[i +      @perm[j +      @perm[k]]]
        gi1 = @gradP[i + i1 + @perm[j + j1 + @perm[k + k1]]]
        gi2 = @gradP[i + i2 + @perm[j + j2 + @perm[k + k2]]]
        gi3 = @gradP[i + 1  + @perm[j + 1  + @perm[k + 1]]]
  • ¶

    Calculate the contribution from the four corners

        t0 = 0.5 - x0*x0-y0*y0-z0*z0
        if(t0<0)
          n0 = 0
        else
          t0 *= t0
          n0 = t0 * t0 * gi0.dot(x0, y0, z0)  # (x,y) of grad3 used for 2D gradient
    
        t1 = 0.5 - x1*x1-y1*y1-z1*z1
        if(t1<0)
          n1 = 0
        else
          t1 *= t1
          n1 = t1 * t1 * gi1.dot(x1, y1, z1)
    
        t2 = 0.5 - x2*x2-y2*y2-z2*z2
        if(t2<0)
          n2 = 0
        else
          t2 *= t2
          n2 = t2 * t2 * gi2.dot(x2, y2, z2)
    
        t3 = 0.5 - x3*x3-y3*y3-z3*z3
        if(t3<0)
          n3 = 0
        else
          t3 *= t3
          n3 = t3 * t3 * gi3.dot(x3, y3, z3)
  • ¶

    Add contributions from each corner to get the final noise value. The result is scaled to return values in the interval [-1,1].

        return 32 * (n0 + n1 + n2 + n3)