seen = {}
if window? then window.seen = seen # for the web
if module?.exports? then module.exports = seen # for node
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.
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 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
}
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
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 Shape
s or Model
s
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. Point
s 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 Point
s magnitude squared. Excludes @w
.
magnitudeSquared : () ->
return @dot(@)
Returns this Point
s 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
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 Color
s 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 : 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
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
}
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()
}
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 RenderModel
s 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
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()
}
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 context
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})
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 @
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 Model
s.
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
}
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)
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
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
))
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
A Scene
is the main object for a view of a scene.
class seen.Scene
defaults: ->
The root model for the scene, which contains Shape
s, Light
s, and
other Model
s
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 = {}
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)