seen = {}
if window? then window.seen = seen # for the web
if module?.exports? then module.exports = seen # for nodeCopyright 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.
Declare and attach seen namespace
seen = {}
if window? then window.seen = seen # for the web
if module?.exports? then module.exports = seen # for node
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 trueReturns 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
}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 eventPool 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.MatrixAccepts a 16-value Array, defaults to the identity matrix.
constructor : (@m = null) ->
@m ?= IDENTITY.slice()
@baked = IDENTITYReturns 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 = IDENTITYWe 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.zComputes 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 : 150Convert 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 resultConvert 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 = nullCreates a copy of this box object with the same bounds
copy : () ->
box = new seen.Bounds()
box.min = @min?.copy()
box.max = @max?.copy()
return boxAdds 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 trueReturns 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 ? 0Color 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 '#' + cConverts 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)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 : falseThe 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 : 15A 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 colorThis 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', optsA 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', optsAmbient lights emit a constant amount of light everywhere at once. Transformation of the light has no effect.
ambient : (opts) -> new seen.Light 'ambient', opts
}
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.ShaderEvery 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 thisThe 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 cThe 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 cThe 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 cThe 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()
}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
}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 contextEach 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()
}
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()
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)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
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 contextA 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 @_inertiaRunningApply 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 = falseAdds 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})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.SurfaceWhen ‘false’ this will override backface culling, which is useful if your
material is transparent. See comment in seen.Scene.
cullBackfaces : trueFill 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 @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 modelVisit 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.004Back light
model.add seen.Lights.directional
normal : seen.P(1, 1, -1).normalize()
intensity : 0.003Fill light
model.add seen.Lights.ambient
intensity : 0.0015
return model
}Polyfill requestAnimationFrame
if window?
requestAnimationFrame =
window.requestAnimationFrame ?
window.mozRequestAnimationFrame ?
window.webkitRequestAnimationFrame ?
window.msRequestAnimationFrame
DEFAULT_FRAME_DELAY = 30 # msecThe 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 = nullStart 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)
elsePerform 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 @_runningcreate 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 = tExecute 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 txnEnqueues 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)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.0Points 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 surfacesAccepts 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.lengthInclude 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.jointsGenerate 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)
returnclass 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
))
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)
superA 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 : trueThe 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 : falseThe 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.surfacesCompute 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.inFrustrumRender 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 renderModelsGet 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 renderModelRemoves 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 = {}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 + tFor 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 * G3Work 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)