├── .gitignore ├── .travis.yml ├── Cakefile ├── LICENSE.md ├── README.md ├── TODO.md ├── dist ├── 0.1.0 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.1.1 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.1.2 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.1.3 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.1.4 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.1.5 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.2.0 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.2.1 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.2.2 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.2.3 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.2.4 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.2.5 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.2.6 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── 0.2.7 │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js └── latest │ ├── seen.coffee │ ├── seen.js │ ├── seen.js.map │ └── seen.min.js ├── package.json ├── publish-site.sh ├── run-site.sh ├── site ├── CNAME ├── assets │ ├── 01_06.bvh │ ├── 05_11.bvh │ ├── berries.jpg │ ├── bunny-low.obj │ ├── bunny.obj │ ├── petal0.obj │ ├── petal1.obj │ ├── teapot-low.obj │ └── teapot.obj ├── css │ └── theme.css ├── demos.coffee ├── docco-template.jst ├── favicons │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-160x160.png │ ├── favicon-16x16.png │ ├── favicon-196x196.png │ ├── favicon-260x260.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ └── mstile-70x70.png ├── index.coffee ├── lib │ ├── 2048 │ │ ├── animframe_polyfill.js │ │ ├── application.js │ │ ├── bind_polyfill.js │ │ ├── classlist_polyfill.js │ │ ├── game_manager.js │ │ ├── grid.js │ │ ├── html_actuator.js │ │ ├── keyboard_input_manager.js │ │ ├── local_storage_manager.js │ │ └── tile.js │ ├── StackBlur.js │ ├── audio-interface.coffee │ ├── gravity.coffee │ └── perlin-noise.coffee ├── markdown │ ├── guide.md │ └── release-notes.md ├── markdowns.coffee ├── options.coffee └── views │ ├── demo-2048.html │ ├── demo-depth-of-field.html │ ├── demo-equalizer.html │ ├── demo-gravity.html │ ├── demo-masks.html │ ├── demo-material-gallery.html │ ├── demo-mocap.html │ ├── demo-multi-views.html │ ├── demo-noisy-patch.html │ ├── demo-noisy-sphere.html │ ├── demo-simple-interactive.html │ ├── demo-svg-canvas.html │ ├── demo-template.html │ ├── demo-text.html │ ├── demo-z-composite.html │ ├── index.html │ ├── markdown-template.html │ └── snippets │ ├── code-block.html │ └── demo-nav.html ├── src ├── affine.coffee ├── animator.coffee ├── bounds.coffee ├── camera.coffee ├── color.coffee ├── events.coffee ├── ext │ ├── bvh-parser.js │ ├── bvh.pegjs │ └── simplex.coffee ├── interaction.coffee ├── light.coffee ├── materials.coffee ├── matrix.coffee ├── model.coffee ├── namespace.coffee ├── point.coffee ├── quaternion.coffee ├── render │ ├── canvas.coffee │ ├── context.coffee │ ├── layers.coffee │ ├── model.coffee │ ├── painters.coffee │ └── svg.coffee ├── scene.coffee ├── shaders.coffee ├── shapes │ ├── bvh-parser.js │ ├── mocap.coffee │ ├── obj.coffee │ └── primitives.coffee ├── surface.coffee ├── transformable.coffee └── util.coffee └── test ├── dev-site ├── dev.coffee ├── index.coffee └── template.html ├── mocha.opts ├── mocha ├── color-test.coffee ├── math-test.coffee ├── model-test.coffee └── render-test.coffee └── phantom ├── canonical ├── canvas-lights-ambient.png ├── canvas-lights-directional.png ├── canvas-lights-point.png ├── canvas-materials-metallic.png ├── canvas-materials-opacity.png ├── canvas-materials-specular.png ├── canvas-materials-stroke.png ├── canvas-shaders-ambient.png ├── canvas-shaders-diffuse.png ├── canvas-shaders-flat.png ├── canvas-shaders-phong.png ├── canvas-shapes-cube.png ├── canvas-shapes-icosahedron-0.png ├── canvas-shapes-icosahedron-1.png ├── canvas-shapes-icosahedron-2.png ├── canvas-shapes-icosahedron-3.png ├── canvas-shapes-icosahedron-4.png ├── canvas-shapes-patch.png ├── canvas-shapes-tetrahedon.png ├── canvas-shapes-text-0.png ├── svg-lights-ambient.png ├── svg-lights-directional.png ├── svg-lights-point.png ├── svg-materials-metallic.png ├── svg-materials-opacity.png ├── svg-materials-specular.png ├── svg-materials-stroke.png ├── svg-shaders-ambient.png ├── svg-shaders-diffuse.png ├── svg-shaders-flat.png ├── svg-shaders-phong.png ├── svg-shapes-cube.png ├── svg-shapes-icosahedron-0.png ├── svg-shapes-icosahedron-1.png ├── svg-shapes-icosahedron-2.png ├── svg-shapes-icosahedron-3.png ├── svg-shapes-icosahedron-4.png ├── svg-shapes-patch.png ├── svg-shapes-tetrahedon.png └── svg-shapes-text-0.png ├── phantom-scenes.coffee ├── render-favicon.coffee ├── render-scenes.coffee └── renders └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | site-dist 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - iojs 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | script: 15 | - npm run build 16 | - npm run test 17 | after_success: 18 | - npm run semantic-release 19 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | require 'coffee-script/register' 2 | 3 | fs = require 'fs' 4 | _ = require 'lodash' 5 | path = require 'path' 6 | UglifyJS = require 'uglify-js' 7 | CoffeeScript = require 'coffee-script' 8 | packageJson = require './package.json' 9 | 10 | {execSync} = require 'child_process' 11 | exec = (cmd) -> execSync(cmd, (err) -> throw err if err) 12 | 13 | DISTS = { 14 | 'seen.js' : [ 15 | 'src/namespace.coffee' 16 | 'src/util.coffee' 17 | 'src/events.coffee' 18 | 'src/matrix.coffee' 19 | 'src/transformable.coffee' 20 | 'src/point.coffee' 21 | 'src/quaternion.coffee' 22 | 'src/bounds.coffee' 23 | 'src/color.coffee' 24 | 'src/materials.coffee' 25 | 'src/light.coffee' 26 | 'src/shaders.coffee' 27 | 'src/affine.coffee' 28 | 'src/render/context.coffee' 29 | 'src/render/painters.coffee' 30 | 'src/render/model.coffee' 31 | 'src/render/layers.coffee' 32 | 'src/render/svg.coffee' 33 | 'src/render/canvas.coffee' 34 | 'src/interaction.coffee' 35 | 'src/surface.coffee' 36 | 'src/model.coffee' 37 | 'src/animator.coffee' 38 | 'src/shapes/primitives.coffee' 39 | 'src/shapes/mocap.coffee' 40 | 'src/shapes/obj.coffee' 41 | 'src/camera.coffee' 42 | 'src/scene.coffee' 43 | # Extensions 44 | 'src/ext/simplex.coffee' 45 | 'src/ext/bvh-parser.js' 46 | ] 47 | } 48 | 49 | MIN_LICENSE = "/** seen.js v#{packageJson.version} | themadcreator.github.io/seen | (c) Bill Dwyer | @license: Apache 2.0 */\n" 50 | 51 | DIST = path.join(__dirname, 'dist', packageJson.version) 52 | SITE_DIST = path.join(__dirname, 'site-dist') 53 | 54 | # ======= 55 | # TASKS 56 | # ======= 57 | 58 | task 'build', 'Build and uglify seen', () -> 59 | 60 | # Prepare output path 61 | if not fs.existsSync(path.join(__dirname, 'dist')) then fs.mkdirSync(path.join(__dirname, 'dist')) 62 | if not fs.existsSync(DIST) then fs.mkdirSync(DIST) 63 | 64 | license = fs.readFileSync(path.join(__dirname, 'LICENSE.md'), 'utf-8') 65 | license = license.split('\n').join('\n# ') 66 | 67 | for javascript, sources of DISTS 68 | console.log "Building #{javascript}..." 69 | 70 | # Concat all coffeescript together for Docco 71 | coffeeCode = sources 72 | .filter((source) -> /\.coffee$/.test(source)) 73 | .map((source) -> fs.readFileSync(source, 'utf-8')).join('\n\n') 74 | coffeeCode = "\n\n# #{license}\n\n" + coffeeCode 75 | fs.writeFileSync path.join(DIST, javascript.replace(/\.js$/, '.coffee')), coffeeCode, {flags: 'w'} 76 | console.log ' Joined.' 77 | 78 | # Compile to javascript 79 | jsCode = CoffeeScript.compile coffeeCode, {bare : true} 80 | otherJsCode = _.chain(sources) 81 | .filter((source) -> /\.js$/.test(source)) 82 | .map((source) -> fs.readFileSync(source, 'utf-8')) 83 | .map((source) -> source.replace(/(?:\/\*(?:[\s\S]*?)\*\/)/gm, '')) # strip comments 84 | .join('\n\n') 85 | .value() 86 | code = """ 87 | #{MIN_LICENSE} 88 | (function(){ 89 | #{jsCode} 90 | #{otherJsCode} 91 | })(this); 92 | """ 93 | fs.writeFileSync path.join(DIST, javascript), code, {flags: 'w'} 94 | console.log ' Compiled.' 95 | 96 | # Uglify 97 | ugly = UglifyJS.minify path.join(DIST, javascript), 98 | outSourceMap : "#{javascript}.map" 99 | output : 100 | comments : true 101 | fs.writeFileSync path.join(DIST, javascript.replace(/\.js$/,'.min.js')), ugly.code, {flags: 'w'} 102 | fs.writeFileSync path.join(DIST, "#{javascript}.map"), ugly.map, {flags: 'w'} 103 | console.log ' Minified.' 104 | 105 | latest = path.join(__dirname, 'dist', 'latest') 106 | exec("rm -rf #{latest}; cp -r #{DIST} #{latest}") 107 | console.log ' Copied to Latest.' 108 | 109 | task 'site', 'Build seen website', (options) -> 110 | console.log "Building static site..." 111 | 112 | swig = require 'swig' 113 | marked = require 'marked' 114 | highlight = require 'highlight.js' 115 | demos = require './site/demos' 116 | markdowns = require './site/markdowns' 117 | pageOptions = require './site/options' 118 | 119 | renderPage = (filename, view, opts) -> 120 | opts = _.defaults(opts, pageOptions) 121 | opts.version = packageJson.version 122 | page = swig.renderFile path.join(__dirname, 'site', 'views', "#{view}.html"), opts 123 | fs.writeFileSync(path.join(SITE_DIST, "#{filename}.html"), page) 124 | 125 | # Prepare output path 126 | exec("rm -rf #{SITE_DIST}") 127 | if not fs.existsSync(SITE_DIST) then fs.mkdirSync(SITE_DIST) 128 | 129 | # Copy static resources 130 | for resource in ['lib', 'css', 'assets', 'CNAME'] 131 | exec("cp -rf site/#{resource} #{SITE_DIST}/#{resource}") # copy site resources 132 | exec("cp site/favicons/* #{SITE_DIST}/.") # copy favicons 133 | exec("cp dist/latest/seen.min.js #{SITE_DIST}/lib/.") # copy dist for demos 134 | exec("cp dist/latest/seen.js #{SITE_DIST}/lib/.") # copy dist for demos 135 | exec("cp -r dist #{SITE_DIST}/dist") # copy dist for download 136 | console.log ' Copied static resources' 137 | 138 | # Generate docco 139 | script = path.join('node_modules' , '.bin', 'docco') 140 | doccoCss = 'node_modules/docco/resources/classic/docco.css' 141 | doccoTemplate = 'site/docco-template.jst' 142 | exec("#{script} --output #{SITE_DIST}/docco --css #{doccoCss} --template #{doccoTemplate} dist/latest/seen.coffee") 143 | console.log ' Generated Docco' 144 | 145 | # Demo pages 146 | for demo, i in demos then do (demo, i) -> 147 | demo.prev = demos[i - 1] 148 | demo.next = demos[i + 1] 149 | renderPage(demo.view, demo.view, demo) 150 | console.log " Rendered '#{demo.title}' demo" 151 | 152 | # Markdowned pages 153 | renderer = new marked.Renderer() 154 | renderer.code = (code, lang, escaped) -> 155 | highlighted = highlight.highlight(lang, code).value 156 | return """
#{highlighted}
"""
157 | marked.setOptions(renderer : renderer)
158 | markdowns.forEach (markdown) ->
159 | content = fs.readFileSync path.join(__dirname, 'site', markdown.path), {encoding : 'UTF-8'}
160 | renderPage(markdown.route, 'markdown-template',
161 | title : markdown.title
162 | markdown : marked(content)
163 | scripts : [pageOptions.cdns.highlightjs.script]
164 | styles : pageOptions.styles.concat [pageOptions.cdns.highlightjs.style]
165 | )
166 | console.log " Rendered '#{markdown.title}' markdown"
167 |
168 | # Index page
169 | renderPage 'index', 'index', {demos}
170 | console.log ' Rendered index'
171 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ## Apache 2.0 License
2 |
3 | Copyright 2013, 2014 github/themadcreator
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # seen.js
2 |
3 | seen.js renders 3D scenes into SVG or HTML5 Canvas.
4 |
5 | Downloads, documentation, and demos on the website: http://seenjs.io
6 |
7 |
8 | ### Installation
9 |
10 | ```
11 | npm install seen
12 | ```
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # Demos
2 | + Origin and units
3 | + Picture in Picture
4 | + Perspective projection diagrams w/ FOV slider
5 | + Area Chart
6 |
7 | # Features
8 | + Shape manipulator control
9 | + Shadow Casting
10 | + Physics
11 | + Isometric projection
12 | + Image Textures
13 |
14 | # Shapes
15 | + Triangulated Torus
16 |
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "seen",
3 | "description" : "seen.js is a library for drawing simple 3D scenes in SVG and HTML5 Canvas elements.",
4 | "version" : "0.2.7",
5 | "author" : "Bill Dwyer",
6 | "license" : "Apache-2.0",
7 | "url" : "http://seenjs.io",
8 | "repository" : {
9 | "type" : "git",
10 | "url" : "https://github.com/themadcreator/seen"
11 | },
12 | "main" : "dist/latest/seen.js",
13 | "keywords" : [
14 | "3d",
15 | "svg",
16 | "canvas"
17 | ],
18 | "scripts": {
19 | "build" : "cake build",
20 | "site" : "nodemon --ext coffee,html,md,css --watch src --watch site --exec ./run-site.sh .",
21 | "publish-site" : "./publish-site.sh",
22 | "test-render" : "phantomjs ./test/phantom/render-scenes.coffee",
23 | "test" : "cake build && mocha ./test/mocha/*.coffee",
24 | "build-bvh-parser" : "pegjs --export-var 'seen.BvhParser' src/ext/bvh.pegjs src/ext/bvh-parser.js"
25 | },
26 | "devDependencies": {
27 | "chai" : ">=1.9.1",
28 | "coffee-script" : ">=1.9.x",
29 | "docco" : ">=0.7.0",
30 | "express" : ">=3.5.0",
31 | "highlight.js" : ">=8.0.0",
32 | "lodash" : ">=2.4.1",
33 | "marked" : ">=0.3.2",
34 | "mocha" : ">=1.18.2",
35 | "nodemon" : ">=1.0.17",
36 | "path" : ">=0.4.9",
37 | "phantomjs" : ">=1.9.7-1",
38 | "pngjs" : ">=0.4.0",
39 | "q" : ">=1.0.1",
40 | "semantic-release" : ">=4.3.5",
41 | "swig" : ">=1.3.2",
42 | "uglify-js" : ">=2.4.13"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/publish-site.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | BRANCH=`git rev-parse --abbrev-ref HEAD`
4 |
5 | if [ $BRANCH != master ]; then
6 | echo "only publish from the master branch. exiting."
7 | exit 1
8 | fi
9 |
10 | cake site
11 |
12 | if [ $? != 0 ]; then
13 | echo "site building failed. exiting."
14 | exit 1
15 | fi
16 |
17 | UNCOMMITED_CHANGES=`git status --short | wc -l`
18 | if [ $UNCOMMITED_CHANGES != 0 ]; then
19 | echo "you have uncommited changes. exiting."
20 | exit 1
21 | fi
22 |
23 | git checkout gh-pages
24 | cp -r site-dist/* .
25 | git add --all .
26 | git commit -m 'updating site'
27 |
28 | echo "\n\nRun 'git push --all' to make site GO LIVE!\n\n"
29 |
--------------------------------------------------------------------------------
/run-site.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Rebuild library and site
4 | cake build site
5 |
6 | # We must use exec here so that signals can propagate to the web server.
7 | # Otherwise, the ports will not close and the restart will fail with
8 | # EADDRINUSE
9 | exec coffee ./site/index.coffee
--------------------------------------------------------------------------------
/site/CNAME:
--------------------------------------------------------------------------------
1 | seenjs.io
--------------------------------------------------------------------------------
/site/assets/berries.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/assets/berries.jpg
--------------------------------------------------------------------------------
/site/assets/petal1.obj:
--------------------------------------------------------------------------------
1 | # Rhino
2 |
3 | v -10 0 0
4 | v -9.3 -0.281 0
5 | v -7.07 -1.17 0
6 | v -5.47 -1.81 0
7 | v -3.68 -2.53 0
8 | v -1.82 -3.27 0
9 | v 0 -4 0
10 | v 0 -3.63 -0.386
11 | v 0 -2.8 -0.658
12 | v 0 -1.73 -0.832
13 | v 0 -0.381 -0.919
14 | v 0 1.19 -0.897
15 | v 0 2.73 -0.773
16 | v 0 3.86 -0.588
17 | v 0 4.66 -0.333
18 | v 0 5 0
19 | v 1.87 -5.09 0.0321
20 | v 3.89 -5.8 0.135
21 | v 3.97 -4.69 -0.172
22 | v 4.16 -3.33 -0.306
23 | v 4.19 6.73 0.135
24 | v 4.4 -1.91 -0.314
25 | v 4.4 5.67 0.0321
26 | v 4.57 4.34 -0.0467
27 | v 4.69 2.88 -0.104
28 | v 4.7 -0.391 -0.201
29 | v 4.77 1.17 -0.142
30 | v 5.96 -6.04 0.313
31 | v 5.97 -5.68 0.212
32 | v 6.07 -4.73 0.0579
33 | v 6.31 -3.28 -0.0166
34 | v 6.35 6.87 0.313
35 | v 6.62 -1.84 0.0363
36 | v 6.62 5.62 0.301
37 | v 6.83 4.2 0.292
38 | v 6.99 2.73 0.285
39 | v 7 -0.366 0.213
40 | v 7.09 1.07 0.281
41 | v 7.95 -5.73 0.563
42 | v 8.06 -4.39 0.363
43 | v 8.32 -2.98 0.336
44 | v 8.36 6.44 0.563
45 | v 8.66 -1.65 0.427
46 | v 8.66 5.15 0.615
47 | v 8.89 3.78 0.655
48 | v 9.06 2.4 0.684
49 | v 9.07 -0.32 0.634
50 | v 9.17 0.925 0.703
51 | v 9.73 -4.88 0.871
52 | v 9.74 -4.53 0.807
53 | v 9.83 -3.68 0.727
54 | v 10.1 -2.46 0.731
55 | v 10.1 5.43 0.871
56 | v 10.4 -1.34 0.835
57 | v 10.4 4.27 0.955
58 | v 10.6 3.09 1.02
59 | v 10.8 1.93 1.07
60 | v 10.8 -0.256 1.04
61 | v 10.9 0.729 1.1
62 | v 11.2 -3.56 1.22
63 | v 11.3 -2.65 1.13
64 | v 11.5 -1.75 1.14
65 | v 11.5 3.93 1.22
66 | v 11.7 -0.94 1.23
67 | v 11.7 3.05 1.3
68 | v 11.9 2.18 1.36
69 | v 12 1.35 1.41
70 | v 12 -0.177 1.4
71 | v 12.1 0.498 1.44
72 | v 12.3 -1.88 1.57
73 | v 13 0 1.92
74 | f 9 8 1
75 | f 20 19 8 9
76 | f 31 30 19 20
77 | f 41 40 30 31
78 | f 52 51 40 41
79 | f 62 61 51 52
80 | f 71 61 62
81 | f 10 9 1
82 | f 22 20 9 10
83 | f 33 31 20 22
84 | f 43 41 31 33
85 | f 54 52 41 43
86 | f 64 62 52 54
87 | f 71 62 64
88 | f 11 10 1
89 | f 26 22 10 11
90 | f 37 33 22 26
91 | f 47 43 33 37
92 | f 58 54 43 47
93 | f 68 64 54 58
94 | f 71 64 68
95 | f 12 11 1
96 | f 27 26 11 12
97 | f 38 37 26 27
98 | f 48 47 37 38
99 | f 59 58 47 48
100 | f 69 68 58 59
101 | f 71 68 69
102 | f 13 12 1
103 | f 25 27 12 13
104 | f 36 38 27 25
105 | f 46 48 38 36
106 | f 57 59 48 46
107 | f 67 69 59 57
108 | f 71 69 67
109 | f 14 13 1
110 | f 24 25 13 14
111 | f 35 36 25 24
112 | f 45 46 36 35
113 | f 56 57 46 45
114 | f 66 67 57 56
115 | f 71 67 66
116 | f 15 14 1
117 | f 23 24 14 15
118 | f 34 35 24 23
119 | f 44 45 35 34
120 | f 55 56 45 44
121 | f 65 66 56 55
122 | f 71 66 65
123 | f 16 15 1
124 | f 21 23 15 16
125 | f 32 34 23 21
126 | f 42 44 34 32
127 | f 53 55 44 42
128 | f 63 65 55 53
129 | f 71 65 63
130 | f 1 3 2
131 | f 8 5 4
132 | f 8 6 5
133 | f 4 3 8
134 | f 8 17 7
135 | f 19 18 17
136 | f 29 39 28
137 | f 50 49 39
138 | f 28 18 29
139 | f 7 6 8
140 | f 17 8 19
141 | f 29 50 39
142 | f 18 19 30 29
143 | f 29 30 40
144 | f 50 29 40
145 | f 61 70 60
146 | f 61 50 51
147 | f 40 51 50
148 | f 61 71 70
149 | f 50 61 60
150 | f 60 49 50
151 | f 8 3 1
152 |
--------------------------------------------------------------------------------
/site/css/theme.css:
--------------------------------------------------------------------------------
1 |
2 | svg text {
3 | font-family : 'Roboto', 'San-serif';
4 | }
5 |
6 | body {
7 | background-color : #AAAAAD;
8 | font-family : 'Roboto', 'San-serif';
9 |
10 | -webkit-touch-callout : none;
11 | -webkit-user-select : none;
12 | -khtml-user-select : none;
13 | -moz-user-select : none;
14 | -ms-user-select : none;
15 | user-select : none;
16 | }
17 |
18 | code {
19 | -webkit-touch-callout : text;
20 | -webkit-user-select : text;
21 | -khtml-user-select : text;
22 | -moz-user-select : text;
23 | -ms-user-select : text;
24 | user-select : text;
25 | }
26 |
27 | pre {
28 | margin: 0px 0px;
29 | }
30 |
31 | .page {
32 | position : absolute;
33 | top : 40px;
34 | width : 900px;
35 | left : 50%;
36 | margin-left : -450px;
37 | margin-bottom : 100px;
38 | background-color : white;
39 | }
40 |
41 | .caption {
42 | text-align : center;
43 | width : 100%;
44 | color : #888;
45 | }
46 |
47 | .footer {
48 | position : relative;
49 | top : 50px;
50 | width : 900px;
51 | left : 50%;
52 | margin-left : -450px;
53 | text-align : center;
54 | color : #888;
55 | }
56 |
57 | .content {
58 | padding : 20px 40px 20px 40px;
59 | color : #444;
60 | }
61 |
62 | h1, h4 {
63 | font-weight : normal;
64 | margin-left : 40px;
65 | color : #444;
66 | }
67 |
68 | a {
69 | text-decoration : none;
70 | color : #09C;
71 | }
72 |
73 | a:visited {
74 | color : #82C;
75 | }
76 |
77 | a:hover {
78 | color : #0AF;
79 | }
80 |
81 | td {
82 | padding : 0px 0px 0px 0px;
83 | }
84 |
85 | ul {
86 | list-style-type: none;
87 | }
88 |
89 | .download-button {
90 | -webkit-box-shadow : inset rgba(255, 255, 255, 0.498039) 0px 1px 0px 0px , rgba(0, 0, 0, 0.14902) 0px 1px 2px 0px;
91 | -moz-box-shadow : inset 0px 1px 0px rgba(255, 255, 255, 0.5), 0px 1px 2px rgba(0, 0, 0, 0.15);
92 | box-shadow : inset 0px 1px 0px rgba(255, 255, 255, 0.5), 0px 1px 2px rgba(0, 0, 0, 0.15);
93 |
94 | color : white;
95 | background-color : rgb(0, 161, 203);
96 | background : -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgb(0, 181, 229)), rgb(0, 141, 178));
97 | background : -webkit-linear-gradient(top, rgb(0, 181, 229), rgb(0, 141, 178));
98 | background : -moz-linear-gradient(top, rgb(0, 181, 229), rgb(0, 141, 178));
99 | background : -o-linear-gradient(top, rgb(0, 181, 229), rgb(0, 141, 178));
100 | background : linear-gradient(top, rgb(0, 181, 229), rgb(0, 141, 178));
101 |
102 | border : 1px solid rgb(0, 121, 152);
103 | border-radius : 3px 3px;
104 |
105 | cursor : auto;
106 |
107 | font-size : 18px;
108 | height : 40px;
109 | line-height : 40px;
110 |
111 | margin : 0 0 0 0;
112 | padding : 0 0 0 0;
113 | text-align : center;
114 |
115 | vertical-align : middle;
116 | width : 365px; // just to align with the words below
117 | }
118 |
119 | .nav-button-wrapper {
120 | display : inline-block;
121 | width : 33%;
122 | }
123 |
124 | .nav-button {
125 | background-color : #F8F8F8;
126 | padding : 1em 3em;
127 | margin : 1em;
128 | }
129 |
130 | .nav-button:hover {
131 | background-color : #EEF;
132 | }
133 |
134 | .nav-button .label {
135 | text-transform : uppercase;
136 | font-size : 1.2em;
137 | }
138 |
139 | .nav-button .title {
140 | font-size : 0.8em;
141 | }
142 |
--------------------------------------------------------------------------------
/site/demos.coffee:
--------------------------------------------------------------------------------
1 | Demos = [
2 | title : 'Hello, World!'
3 | view : 'demo-simple-interactive'
4 | width : 900
5 | height : 500
6 | ,
7 | title : 'Materials gallery'
8 | view : 'demo-material-gallery'
9 | width : 900
10 | height : 500
11 | ,
12 | title : 'Noisy Wave Patch'
13 | view : 'demo-noisy-patch'
14 | width : 900
15 | height : 500
16 | ,
17 | title : 'Noisy Sphere'
18 | view : 'demo-noisy-sphere'
19 | width : 900
20 | height : 500
21 | ,
22 | title : 'Same Scene, Canvas vs. SVG'
23 | view : 'demo-svg-canvas'
24 | ,
25 | title : 'Same Scene, Multiple Angles'
26 | view : 'demo-multi-views'
27 | ,
28 | title : 'SVG Masks and Effects'
29 | view : 'demo-masks'
30 | width : 900
31 | height : 500
32 | ,
33 | title : 'Text'
34 | view : 'demo-text'
35 | width : 900
36 | height : 500
37 | ,
38 | title : 'Depth of Field'
39 | view : 'demo-depth-of-field'
40 | width : 900
41 | height : 500
42 | ,
43 | title : 'Z-Buffer Compositing'
44 | view : 'demo-z-composite'
45 | width : 900
46 | height : 500
47 | ,
48 | title : 'Audio Equalizer'
49 | view : 'demo-equalizer'
50 | ,
51 | title : 'N-Body Gravity Simulation'
52 | view : 'demo-gravity'
53 | width : 900
54 | height : 500
55 | ,
56 | title : 'Mocap-Driven Skeleton'
57 | view : 'demo-mocap'
58 | width : 900
59 | height : 500
60 | ,
61 | title : '2048'
62 | view : 'demo-2048'
63 | width : 900
64 | height : 500
65 | ]
66 |
67 | module.exports = Demos
68 |
--------------------------------------------------------------------------------
/site/docco-template.jst:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Depth of field effect using StackBlur.js.
5 | {% endblock %} 6 | 7 | {% block demo %} 8 | 9 | 128 | {% endblock %} 129 | -------------------------------------------------------------------------------- /site/views/demo-equalizer.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block canvases %} 4 | 8 | 9 | {% endblock %} 10 | 11 | {% block caption %} 12 |Audio : Battar by EOTO (CC Attribution 3.0)
13 | {% endblock %} 14 | 15 | {% block demo %} 16 | 17 | 88 | {% endblock %} 89 | 90 | -------------------------------------------------------------------------------- /site/views/demo-gravity.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block caption %} 4 |Drag to rotate. Mousewheel to zoom.
5 | {% endblock %} 6 | 7 | {% block demo %} 8 | 9 | 10 | 114 | {% endblock %} 115 | -------------------------------------------------------------------------------- /site/views/demo-material-gallery.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block demo %} 4 | 5 | 6 | 7 | 8 | 9 | 27 | 28 | 29 |Drag to rotate. Mousewheel to zoom.
5 | {% endblock %} 6 | 7 | {% block demo %} 8 | 9 | 10 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /site/views/demo-multi-views.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block canvases %} 4 |7 | | 8 | | 9 | |
12 | | 13 | |
Drag on large view to rotate model.
19 | {% endblock %} 20 | 21 | {% block demo %} 22 | 23 | 75 | {% endblock %} 76 | 77 | -------------------------------------------------------------------------------- /site/views/demo-noisy-patch.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block demo %} 4 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /site/views/demo-noisy-sphere.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block caption %} 4 |Drag to rotate.
5 | {% endblock %} 6 | 7 | {% block demo %} 8 | 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /site/views/demo-simple-interactive.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block caption %} 4 |Drag to rotate.
5 | {% endblock %} 6 | 7 | {% block demo %} 8 | 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /site/views/demo-svg-canvas.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block canvases %} 4 |HTML5 Canvas | SVG |
---|---|
Drag to rotate.
5 | {% endblock %} 6 | 7 | {% block demo %} 8 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /site/views/demo-z-composite.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block canvases %} 4 |7 | | 8 | |
Left: Default painters algorithm. Right: Z-buffer compositing
14 | {% endblock %} 15 | 16 | {% block demo %} 17 | 18 | 188 | {% endblock %} 189 | -------------------------------------------------------------------------------- /site/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |28 | source map | 29 | unminified javascript | 30 | full coffeescript 31 |
32 | 33 |seen.js has no dependencies.
34 | 35 |Licensed under Apache 2.0
36 | 37 |To see what is new in this version, read the release notes.
38 | 39 |
4 |
--------------------------------------------------------------------------------
/site/views/snippets/demo-nav.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/affine.coffee:
--------------------------------------------------------------------------------
1 | # ## Affine
2 | # #### Fake projections with affine transforms
3 | # ------------------
4 | #
5 | # It is not possible exactly render text in a scene with a perspective
6 | # projection because Canvas and SVG support only affine transformations. So,
7 | # in order to fake it, we create an affine transform that approximates the
8 | # linear effects of a perspective projection on an unrendered planar surface
9 | # that represents the text's shape. We can use this transform directly in the
10 | # text painter to warp the text.
11 | #
12 | # This fake projection will produce unrealistic results with large strings of
13 | # text that are not broken into their own shapes.
14 | seen.Affine = {
15 |
16 | # This is the set of points that must be used by a surface that will use an
17 | # affine transform for rendering.
18 | ORTHONORMAL_BASIS : -> [
19 | seen.P( 0, 0, 0)
20 | seen.P(20, 0, 0)
21 | seen.P( 0, 20, 0)
22 | ]
23 |
24 | # This matrix is built using the method from this StackOverflow answer:
25 | # http://stackoverflow.com/questions/22954239/given-three-points-compute-affine-transformation
26 | #
27 | # We further re-arranged the rows to avoid having to do any matrix factorization.
28 | INITIAL_STATE_MATRIX : [
29 | [20, 0, 1, 0, 0, 0]
30 | [ 0, 20, 1, 0, 0, 0]
31 | [ 0, 0, 1, 0, 0, 0]
32 | [ 0, 0, 0, 20, 0, 1]
33 | [ 0, 0, 0, 0, 20, 1]
34 | [ 0, 0, 0, 0, 0, 1]
35 | ]
36 |
37 | # Computes the parameters of an affine transform from the 3 projected
38 | # points.
39 | #
40 | # Because we control the initial values of the points, we can re-use the
41 | # state matrix. Furthermore, because we have use a special layout (upper
42 | # triangular) for this matrix, we avoid any matrix factorization and can go
43 | # directly to back-substitution to solve the matrix equation.
44 | #
45 | # To use the affine transform, use the indices like so (note that we flip y):
46 | # x[0], x[3], -x[1], -x[4], x[2], x[5]
47 | solveForAffineTransform : (points) ->
48 | A = seen.Affine.INITIAL_STATE_MATRIX
49 |
50 | b = [
51 | points[1].x
52 | points[2].x
53 | points[0].x
54 | points[1].y
55 | points[2].y
56 | points[0].y
57 | ]
58 |
59 | # Use back substitution to solve A*x=b for x
60 | x = new Array(6)
61 | n = A.length
62 | for i in [(n-1)..0] by -1
63 | x[i] = b[i]
64 | for j in [(i+1)...n]
65 | x[i] -= A[i][j] * x[j]
66 | x[i] /= A[i][i]
67 |
68 | return x
69 | }
70 |
--------------------------------------------------------------------------------
/src/animator.coffee:
--------------------------------------------------------------------------------
1 | # ## Animator
2 | # ------------------
3 |
4 | # Polyfill requestAnimationFrame
5 | if window?
6 | requestAnimationFrame =
7 | window.requestAnimationFrame ?
8 | window.mozRequestAnimationFrame ?
9 | window.webkitRequestAnimationFrame ?
10 | window.msRequestAnimationFrame
11 |
12 | DEFAULT_FRAME_DELAY = 30 # msec
13 |
14 | # The animator class is useful for creating an animation loop. We supply pre
15 | # and post events for apply animation changes between frames.
16 | class seen.Animator
17 | constructor : () ->
18 | @dispatch = seen.Events.dispatch('beforeFrame', 'afterFrame', 'frame')
19 | @on = @dispatch.on
20 | @timestamp = 0
21 | @_running = false
22 | @frameDelay = null
23 |
24 | # Start the animation loop.
25 | start : ->
26 | @_running = true
27 |
28 | if @frameDelay?
29 | @_lastTime = new Date().valueOf()
30 | @_delayCompensation = 0
31 |
32 | @animateFrame()
33 | return @
34 |
35 | # Stop the animation loop.
36 | stop : ->
37 | @_running = false
38 | return @
39 |
40 | # Use requestAnimationFrame if available and we have no explicit frameDelay.
41 | # Otherwise, use a delay-compensated timeout.
42 | animateFrame : ->
43 | if requestAnimationFrame? and not @frameDelay?
44 | requestAnimationFrame(@frame)
45 | else
46 | # Perform frame delay compensation to make sure each frame is rendered at
47 | # the right time. This makes some animations more consistent
48 | delta = new Date().valueOf() - @_lastTime
49 | @_lastTime += delta
50 | @_delayCompensation += delta
51 |
52 | frameDelay = @frameDelay ? DEFAULT_FRAME_DELAY
53 | setTimeout(@frame, frameDelay - @_delayCompensation)
54 | return @
55 |
56 | # The main animation frame method
57 | frame : (t) =>
58 | return unless @_running
59 |
60 | # create timestamp param even if requestAnimationFrame isn't available
61 | @_timestamp = t ? (@_timestamp + (@_msecDelay ? DEFAULT_FRAME_DELAY))
62 | deltaTimestamp = if @_lastTimestamp? then @_timestamp - @_lastTimestamp else @_timestamp
63 |
64 | @dispatch.beforeFrame(@_timestamp, deltaTimestamp)
65 | @dispatch.frame(@_timestamp, deltaTimestamp)
66 | @dispatch.afterFrame(@_timestamp, deltaTimestamp)
67 |
68 | @_lastTimestamp = @_timestamp
69 |
70 | @animateFrame()
71 | return @
72 |
73 | # Add a callback that will be invoked before the frame
74 | onBefore : (handler) ->
75 | @on "beforeFrame.#{seen.Util.uniqueId('animator-')}", handler
76 | return @
77 |
78 | # Add a callback that will be invoked after the frame
79 | onAfter : (handler) ->
80 | @on "afterFrame.#{seen.Util.uniqueId('animator-')}", handler
81 | return @
82 |
83 | # Add a frame callback
84 | onFrame : (handler) ->
85 | @on "frame.#{seen.Util.uniqueId('animator-')}", handler
86 | return @
87 |
88 | # A seen.Animator for rendering the seen.Context
89 | class seen.RenderAnimator extends seen.Animator
90 | constructor : (context) ->
91 | super
92 | @onFrame(context.render)
93 |
94 | # A transition object to manage to animation of shapes
95 | class seen.Transition
96 | defaults :
97 | duration : 100 # The duration of this transition in msec
98 |
99 | constructor : (options = {}) ->
100 | seen.Util.defaults(@, options, @defaults)
101 |
102 | update : (t) ->
103 | # Setup the first frame before the tick increment
104 | if not @t?
105 | @firstFrame()
106 | @startT = t
107 |
108 | # Execute a tick and draw a frame
109 | @t = t
110 | @tFrac = (@t - @startT) / @duration
111 | @frame()
112 |
113 | # Cleanup or update on last frame after tick
114 | if (@tFrac >= 1.0)
115 | @lastFrame()
116 | return false
117 |
118 | return true
119 |
120 | firstFrame : ->
121 | frame : ->
122 | lastFrame : ->
123 |
124 | # A seen.Animator for updating seen.Transtions. We include keyframing to make
125 | # sure we wait for one transition to finish before starting the next one.
126 | class seen.TransitionAnimator extends seen.Animator
127 | constructor : ->
128 | super
129 | @queue = []
130 | @transitions = []
131 | @onFrame(@update)
132 |
133 | # Adds a transition object to the current set of transitions. Note that
134 | # transitions will not start until they have been enqueued by invoking
135 | # `keyframe()` on this object.
136 | add : (txn) ->
137 | @transitions.push txn
138 |
139 | # Enqueues the current set of transitions into the keyframe queue and sets
140 | # up a new set of transitions.
141 | keyframe : ->
142 | @queue.push @transitions
143 | @transitions = []
144 |
145 | # When this animator updates, it invokes `update()` on all of the
146 | # currently animating transitions. If any of the current transitions are
147 | # not done, we re-enqueue them at the front. If all transitions are
148 | # complete, we will start animating the next set of transitions from the
149 | # keyframe queue on the next update.
150 | update : (t) =>
151 | return unless @queue.length
152 | transitions = @queue.shift()
153 | transitions = transitions.filter (transition) -> transition.update(t)
154 | if transitions.length then @queue.unshift(transitions)
155 |
156 |
--------------------------------------------------------------------------------
/src/bounds.coffee:
--------------------------------------------------------------------------------
1 | # The `Bounds` object contains an axis-aligned bounding box.
2 | class seen.Bounds
3 |
4 | @points : (points) ->
5 | box = new seen.Bounds()
6 | box.add(p) for p in points
7 | return box
8 |
9 | @xywh : (x, y, w, h) ->
10 | return seen.Boundses.xyzwhd(x, y, 0, w, h, 0)
11 |
12 | @xyzwhd : (x, y, z, w, h, d) ->
13 | box = new seen.Bounds()
14 | box.add(seen.P(x, y, z))
15 | box.add(seen.P(x+w, y+h, z+d))
16 | return box
17 |
18 | constructor : () ->
19 | @min = null
20 | @max = null
21 |
22 | # Creates a copy of this box object with the same bounds
23 | copy : () ->
24 | box = new seen.Bounds()
25 | box.min = @min?.copy()
26 | box.max = @max?.copy()
27 | return box
28 |
29 | # Adds this point to the bounding box, extending it if necessary
30 | add : (p) ->
31 | if not (@min? and @max?)
32 | @min = p.copy()
33 | @max = p.copy()
34 | else
35 | @min.x = Math.min(@min.x, p.x)
36 | @min.y = Math.min(@min.y, p.y)
37 | @min.z = Math.min(@min.z, p.z)
38 |
39 | @max.x = Math.max(@max.x, p.x)
40 | @max.y = Math.max(@max.y, p.y)
41 | @max.z = Math.max(@max.z, p.z)
42 | return @
43 |
44 | # Returns true of this box contains at least one point
45 | valid : ->
46 | return (@min? and @max?)
47 |
48 | # Trims this box so that it results in the intersection of this box and the
49 | # supplied box.
50 | intersect : (box) ->
51 | if not @valid() or not box.valid()
52 | @min = null
53 | @max = null
54 | else
55 | @min = seen.P(
56 | Math.max(@min.x, box.min.x)
57 | Math.max(@min.y, box.min.y)
58 | Math.max(@min.z, box.min.z)
59 | )
60 | @max = seen.P(
61 | Math.min(@max.x, box.max.x)
62 | Math.min(@max.y, box.max.y)
63 | Math.min(@max.z, box.max.z)
64 | )
65 | if @min.x > @max.x or @min.y > @max.y or @min.z > @max.z
66 | @min = null
67 | @max = null
68 | return @
69 |
70 |
71 | # Pads the min and max of this box using the supplied x, y, and z
72 | pad : (x, y, z) ->
73 | if @valid()
74 | y ?= x
75 | z ?= y
76 | p = seen.P(x,y,z)
77 | @min.subtract(p)
78 | @max.add(p)
79 | return @
80 |
81 | # Returns this bounding box to an empty state
82 | reset : () ->
83 | @min = null
84 | @max = null
85 | return @
86 |
87 | # Return true iff the point p lies within this bounding box. Points on the
88 | # edge of the box are included.
89 | contains : (p) ->
90 | if not @valid()
91 | return false
92 | else if @min.x > p.x or @max.x < p.x
93 | return false
94 | else if @min.y > p.y or @max.y < p.y
95 | return false
96 | else if @min.z > p.z or @max.z < p.z
97 | return false
98 | else
99 | return true
100 |
101 | # Returns the center of the box or zero if no points are in the box
102 | center : () ->
103 | return seen.P(
104 | @minX() + @width()/2
105 | @minY() + @height()/2
106 | @minZ() + @depth()/2
107 | )
108 |
109 | # Returns the width (x extent) of the box
110 | width : () => @maxX() - @minX()
111 |
112 | # Returns the height (y extent) of the box
113 | height : () => @maxY() - @minY()
114 |
115 | # Returns the depth (z extent) of the box
116 | depth : () => @maxZ() - @minZ()
117 |
118 | minX : () => return @min?.x ? 0
119 | minY : () => return @min?.y ? 0
120 | minZ : () => return @min?.z ? 0
121 |
122 | maxX : () => return @max?.x ? 0
123 | maxY : () => return @max?.y ? 0
124 | maxZ : () => return @max?.z ? 0
125 |
--------------------------------------------------------------------------------
/src/camera.coffee:
--------------------------------------------------------------------------------
1 | # ## Camera
2 | # #### Projections, Viewports, and Cameras
3 | # ------------------
4 |
5 | # These projection methods return a 3D to 2D `Matrix` transformation.
6 | # Each projection assumes the camera is located at (0,0,0).
7 | seen.Projections = {
8 | # Creates a perspective projection matrix
9 | perspectiveFov : (fovyInDegrees = 50, front = 1) ->
10 | tan = front * Math.tan(fovyInDegrees * Math.PI / 360.0)
11 | return seen.Projections.perspective(-tan, tan, -tan, tan, front, 2*front)
12 |
13 | # Creates a perspective projection matrix with the supplied frustrum
14 | perspective : (left=-1, right=1, bottom=-1, top=1, near=1, far=100) ->
15 | near2 = 2 * near
16 | dx = right - left
17 | dy = top - bottom
18 | dz = far - near
19 |
20 | m = new Array(16)
21 | m[0] = near2 / dx
22 | m[1] = 0.0
23 | m[2] = (right + left) / dx
24 | m[3] = 0.0
25 |
26 | m[4] = 0.0
27 | m[5] = near2 / dy
28 | m[6] = (top + bottom) / dy
29 | m[7] = 0.0
30 |
31 | m[8] = 0.0
32 | m[9] = 0.0
33 | m[10] = -(far + near) / dz
34 | m[11] = -(far * near2) / dz
35 |
36 | m[12] = 0.0
37 | m[13] = 0.0
38 | m[14] = -1.0
39 | m[15] = 0.0
40 | return seen.M(m)
41 |
42 | # Creates a orthographic projection matrix with the supplied frustrum
43 | ortho : (left=-1, right=1, bottom=-1, top=1, near=1, far=100) ->
44 | near2 = 2 * near
45 | dx = right - left
46 | dy = top - bottom
47 | dz = far - near
48 |
49 | m = new Array(16)
50 | m[0] = 2 / dx
51 | m[1] = 0.0
52 | m[2] = 0.0
53 | m[3] = (right + left) / dx
54 |
55 | m[4] = 0.0
56 | m[5] = 2 / dy
57 | m[6] = 0.0
58 | m[7] = -(top + bottom) / dy
59 |
60 | m[8] = 0.0
61 | m[9] = 0.0
62 | m[10] = -2 / dz
63 | m[11] = -(far + near) / dz
64 |
65 | m[12] = 0.0
66 | m[13] = 0.0
67 | m[14] = 0.0
68 | m[15] = 1.0
69 | return seen.M(m)
70 | }
71 |
72 | seen.Viewports = {
73 | # Create a viewport where the scene's origin is centered in the view
74 | center : (width = 500, height = 500, x = 0, y = 0) ->
75 | prescale = seen.M()
76 | .translate(-x, -y, -height)
77 | .scale(1/width, 1/height, 1/height)
78 |
79 | postscale = seen.M()
80 | .scale(width, -height, height)
81 | .translate(x + width/2, y + height/2, height)
82 | return {prescale, postscale}
83 |
84 | # Create a view port where the scene's origin is aligned with the origin ([0, 0]) of the view
85 | origin : (width = 500, height = 500, x = 0, y = 0) ->
86 | prescale = seen.M()
87 | .translate(-x, -y, -1)
88 | .scale(1/width, 1/height, 1/height)
89 |
90 | postscale = seen.M()
91 | .scale(width, -height, height)
92 | .translate(x, y)
93 | return {prescale, postscale}
94 | }
95 |
96 | # The `Camera` model contains all three major components of the 3D to 2D tranformation.
97 | #
98 | # First, we transform object from world-space (the same space that the coordinates of
99 | # surface points are in after all their transforms are applied) to camera space. Typically,
100 | # this will place all viewable objects into a cube with coordinates:
101 | # x = -1 to 1, y = -1 to 1, z = 1 to 2
102 | #
103 | # Second, we apply the projection trasform to create perspective parallax and what not.
104 | #
105 | # Finally, we rescale to the viewport size.
106 | #
107 | # These three steps allow us to easily create shapes whose coordinates match up to
108 | # screen coordinates in the z = 0 plane.
109 | class seen.Camera extends seen.Transformable
110 | defaults :
111 | projection : seen.Projections.perspective()
112 |
113 | constructor : (options) ->
114 | seen.Util.defaults(@, options, @defaults)
115 | super
116 |
117 |
--------------------------------------------------------------------------------
/src/color.coffee:
--------------------------------------------------------------------------------
1 | # ## Colors
2 | # ------------------
3 |
4 | # `Color` objects store RGB and Alpha values from 0 to 255.
5 | class seen.Color
6 | constructor : (@r = 0, @g = 0, @b = 0, @a = 0xFF) ->
7 |
8 | # Returns a new `Color` object with the same rgb and alpha values as the current object
9 | copy : () ->
10 | return new seen.Color(@r, @g, @b, @a)
11 |
12 | # Scales the rgb channels by the supplied scalar value.
13 | scale : (n) ->
14 | @r *= n
15 | @g *= n
16 | @b *= n
17 | return @
18 |
19 | # Offsets each rgb channel by the supplied scalar value.
20 | offset : (n) ->
21 | @r += n
22 | @g += n
23 | @b += n
24 | return @
25 |
26 | # Clamps each rgb channel to the supplied minimum and maximum scalar values.
27 | clamp : (min = 0, max = 0xFF) ->
28 | @r = Math.min(max, Math.max(min, @r))
29 | @g = Math.min(max, Math.max(min, @g))
30 | @b = Math.min(max, Math.max(min, @b))
31 | return @
32 |
33 | # Takes the minimum between each channel of this `Color` and the supplied `Color` object.
34 | minChannels : (c) ->
35 | @r = Math.min(c.r, @r)
36 | @g = Math.min(c.g, @g)
37 | @b = Math.min(c.b, @b)
38 | return @
39 |
40 | # Adds the channels of the current `Color` with each respective channel from the supplied `Color` object.
41 | addChannels : (c) ->
42 | @r += c.r
43 | @g += c.g
44 | @b += c.b
45 | return @
46 |
47 | # Multiplies the channels of the current `Color` with each respective channel from the supplied `Color` object.
48 | multiplyChannels : (c) ->
49 | @r *= c.r
50 | @g *= c.g
51 | @b *= c.b
52 | return @
53 |
54 | # Converts the `Color` into a hex string of the form "#RRGGBB".
55 | hex : () ->
56 | c = (@r << 16 | @g << 8 | @b).toString(16)
57 | while (c.length < 6) then c = '0' + c
58 | return '#' + c
59 |
60 | # Converts the `Color` into a CSS-style string of the form "rgba(RR, GG, BB, AA)"
61 | style : () ->
62 | return "rgba(#{@r},#{@g},#{@b},#{@a})"
63 |
64 | seen.Colors = {
65 | CSS_RGBA_STRING_REGEX : /rgb(a)?\(([0-9.]+),([0-9.]+),*([0-9.]+)(,([0-9.]+))?\)/
66 |
67 | # Parses a hex string starting with an octothorpe (#) or an rgb/rgba CSS
68 | # string. Note that the CSS rgba format uses a float value of 0-1.0 for
69 | # alpha, but seen uses an in from 0-255.
70 | parse : (str) ->
71 | if str.charAt(0) is '#' and str.length is 7
72 | return seen.Colors.hex(str)
73 | else if str.indexOf('rgb') is 0
74 | m = seen.Colors.CSS_RGBA_STRING_REGEX.exec(str)
75 | return seen.Colors.black() unless m?
76 | a = if m[6]? then Math.round(parseFloat(m[6]) * 0xFF) else undefined
77 | return new seen.Color(parseFloat(m[2]), parseFloat(m[3]), parseFloat(m[4]), a)
78 | else
79 | return seen.Colors.black()
80 |
81 | # Creates a new `Color` using the supplied rgb and alpha values.
82 | #
83 | # Each value must be in the range [0, 255] or, equivalently, [0x00, 0xFF].
84 | rgb : (r, g, b, a = 255) ->
85 | return new seen.Color(r, g, b, a)
86 |
87 | # Creates a new `Color` using the supplied hex string of the form "#RRGGBB".
88 | hex : (hex) ->
89 | hex = hex.substring(1) if (hex.charAt(0) is '#')
90 | return new seen.Color(
91 | parseInt(hex.substring(0, 2), 16),
92 | parseInt(hex.substring(2, 4), 16),
93 | parseInt(hex.substring(4, 6), 16))
94 |
95 | # Creates a new `Color` using the supplied hue, saturation, and lightness
96 | # (HSL) values.
97 | #
98 | # Each value must be in the range [0.0, 1.0].
99 | hsl : (h, s, l, a = 1) ->
100 | r = g = b = 0
101 | if (s == 0)
102 | # When saturation is 0, the color is "achromatic" or "grayscale".
103 | r = g = b = l
104 | else
105 | hue2rgb = (p, q, t) ->
106 | if (t < 0)
107 | t += 1
108 | else if (t > 1)
109 | t -= 1
110 |
111 | if (t < 1 / 6)
112 | return p + (q - p) * 6 * t
113 | else if (t < 1 / 2)
114 | return q
115 | else if (t < 2 / 3)
116 | return p + (q - p) * (2 / 3 - t) * 6
117 | else
118 | return p
119 |
120 | q = if l < 0.5 then l * (1 + s) else l + s - l * s
121 | p = 2 * l - q
122 | r = hue2rgb(p, q, h + 1 / 3)
123 | g = hue2rgb(p, q, h)
124 | b = hue2rgb(p, q, h - 1 / 3)
125 |
126 | return new seen.Color(r * 255, g * 255, b * 255, a * 255)
127 |
128 | # Generates a new random color for each surface of the supplied `Shape`.
129 | randomSurfaces : (shape, sat = 0.5, lit = 0.4) ->
130 | for surface in shape.surfaces
131 | surface.fill seen.Colors.hsl(Math.random(), sat, lit)
132 |
133 | # Generates a random hue then randomly drifts the hue for each surface of
134 | # the supplied `Shape`.
135 | randomSurfaces2 : (shape, drift = 0.03, sat = 0.5, lit = 0.4) ->
136 | hue = Math.random()
137 | for surface in shape.surfaces
138 | hue += (Math.random() - 0.5) * drift
139 | while hue < 0 then hue += 1
140 | while hue > 1 then hue -= 1
141 | surface.fill seen.Colors.hsl(hue, 0.5, 0.4)
142 |
143 | # Generates a random color then sets the fill for every surface of the
144 | # supplied `Shape`.
145 | randomShape : (shape, sat = 0.5, lit = 0.4) ->
146 | shape.fill new seen.Material seen.Colors.hsl(Math.random(), sat, lit)
147 |
148 | # A few `Color`s are supplied for convenience.
149 | black : -> @hex('#000000')
150 | white : -> @hex('#FFFFFF')
151 | gray : -> @hex('#888888')
152 | }
153 |
154 | # Convenience `Color` constructor.
155 | seen.C = (r,g,b,a) -> new seen.Color(r,g,b,a)
156 |
157 |
--------------------------------------------------------------------------------
/src/events.coffee:
--------------------------------------------------------------------------------
1 | # ## Events
2 | # ------------------
3 |
4 | # Attribution: these have been adapted from d3.js's event dispatcher
5 | # functions.
6 |
7 | seen.Events = {
8 | # Return a new dispatcher that creates event types using the supplied string
9 | # argument list. The returned `Dispatcher` will have methods with the names
10 | # of the event types.
11 | dispatch : () ->
12 | dispatch = new seen.Events.Dispatcher()
13 | for arg in arguments
14 | dispatch[arg] = seen.Events.Event()
15 | return dispatch
16 | }
17 |
18 | # The `Dispatcher` class. These objects have methods that can be invoked like
19 | # `dispatch.eventName()`. Listeners can be registered with
20 | # `dispatch.on('eventName.uniqueId', callback)`. Listeners can be removed with
21 | # `dispatch.on('eventName.uniqueId', null)`. Listeners can also be registered
22 | # and removed with `dispatch.eventName.on('name', callback)`.
23 | #
24 | # Note that only one listener with the name event name and id can be
25 | # registered at once. If you to generate unique ids, you can use the
26 | # seen.Util.uniqueId() method.
27 | class seen.Events.Dispatcher
28 | on : (type, listener) =>
29 | i = type.indexOf '.'
30 | name = ''
31 | if i > 0
32 | name = type.substring(i + 1)
33 | type = type.substring(0, i)
34 |
35 | if @[type]?
36 | @[type].on(name, listener)
37 |
38 | return @
39 |
40 | # Internal event object for storing listener callbacks and a map for easy
41 | # lookup. This method returns a new event object.
42 | seen.Events.Event = ->
43 |
44 | # Invokes all of the listeners using the supplied arguments.
45 | event = ->
46 | for name, l of event.listenerMap
47 | if l? then l.apply(@, arguments)
48 |
49 | # Stores listeners for this event
50 | event.listenerMap = {}
51 |
52 | # Connects a listener to the event, deleting any other listener with the
53 | # same name.
54 | event.on = (name, listener) ->
55 | delete event.listenerMap[name]
56 | if listener? then event.listenerMap[name] = listener
57 |
58 | return event
59 |
--------------------------------------------------------------------------------
/src/ext/bvh.pegjs:
--------------------------------------------------------------------------------
1 |
2 | /*----------------------------------------------
3 |
4 | Biovision BVH motion capture format
5 |
6 | License : Apache-2.0
7 | Author : themadcreator@github
8 |
9 | To build parser:
10 | > npm install -g pegjs
11 | > pegjs bvh.pegjs bvh-parser.js
12 |
13 | -----------------------------------------------*/
14 |
15 | program
16 | = 'HIERARCHY'i _ root:root _ motion:motion
17 | { return {root:root, motion:motion} }
18 |
19 | /*----------------------------------------------
20 |
21 | JOINT HIERARCHY
22 |
23 | -----------------------------------------------*/
24 |
25 | root
26 | = 'ROOT'i _ id:id _ '{' _ offset:offset _ channels:channels _ joints:joint* '}'
27 | { return {id:id, offset:offset, channels:channels, joints:joints}}
28 |
29 | joint
30 | = 'JOINT'i _ id:id _ '{' _ offset:offset _ channels:channels _ joints:joint* '}' _
31 | { return {type:'JOINT', id:id, offset:offset, channels:channels, joints:joints} }
32 | / 'END SITE'i _ '{' _ offset:offset _ '}' _
33 | { return {type:'END SITE', offset:offset}}
34 |
35 | offset
36 | = 'OFFSET'i _ x:float _ y:float _ z:float
37 | { return {x:x, y:y, z:z} }
38 |
39 | channels
40 | = 'CHANNELS'i _ count:[0-9] _ channels:(channel_type*)
41 | { return channels }
42 |
43 | channel_type
44 | = channel_type:(
45 | 'Xposition'i /
46 | 'Yposition'i /
47 | 'Zposition'i /
48 | 'Xrotation'i /
49 | 'Yrotation'i /
50 | 'Zrotation'i
51 | ) _
52 | { return channel_type }
53 |
54 | /*----------------------------------------------
55 |
56 | MOTION DATA FRAMES
57 |
58 | -----------------------------------------------*/
59 |
60 | motion
61 | = 'MOTION'i _ 'Frames:'i _ frameCount:integer _ 'Frame Time:'i _ frameTime:float _ frames:frame_data*
62 | { return {frameCount:frameCount, frameTime:frameTime, frames:frames} }
63 |
64 | frame_data
65 | = frameValues:(frame_value+) [\n\r]+
66 | { return frameValues }
67 |
68 | frame_value
69 | = value:float [ ]*
70 | { return value }
71 |
72 | /*----------------------------------------------
73 |
74 | VALUE TYPES
75 |
76 | -----------------------------------------------*/
77 |
78 | id
79 | = $([a-zA-Z0-9-_]+)
80 |
81 | float
82 | = value:[-0-9.e]+
83 | { return parseFloat(value.join('')) }
84 |
85 | integer
86 | = value:[-0-9e]+
87 | { return parseInt(value.join('')) }
88 | _
89 | = [ \t\n\r]*
90 | { return undefined }
91 |
--------------------------------------------------------------------------------
/src/ext/simplex.coffee:
--------------------------------------------------------------------------------
1 | # Adapted from https://github.com/josephg/noisejs/blob/master/perlin.js
2 |
3 | # This code was placed in the public domain by its original author,
4 | # Stefan Gustavson. You may use it as you see fit, but
5 | # attribution is appreciated.
6 |
7 | class seen.Grad
8 | constructor : (@x, @y, @z) ->
9 | dot : (x, y, z) -> @x*x + @y*y + @z*z
10 |
11 | grad3 = [
12 | new seen.Grad( 1, 1, 0)
13 | new seen.Grad(-1, 1, 0)
14 | new seen.Grad( 1,-1, 0)
15 | new seen.Grad(-1,-1, 0)
16 | new seen.Grad( 1, 0, 1)
17 | new seen.Grad(-1, 0, 1)
18 | new seen.Grad( 1, 0,-1)
19 | new seen.Grad(-1, 0,-1)
20 | new seen.Grad( 0, 1, 1)
21 | new seen.Grad( 0,-1, 1)
22 | new seen.Grad( 0, 1,-1)
23 | new seen.Grad( 0,-1,-1)
24 | ]
25 |
26 | # To remove the need for index wrapping, double the permutation table length
27 | SIMPLEX_PERMUTATIONS_TABLE = [151,160,137,91,90,15,
28 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
29 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
30 | 88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,
31 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
32 | 102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,
33 | 135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,
34 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
35 | 223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,
36 | 129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,
37 | 251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,
38 | 49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,
39 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]
40 |
41 | F3 = 1 / 3
42 | G3 = 1 / 6
43 |
44 | class seen.Simplex3D
45 | constructor : (seed = 0) ->
46 | @perm = new Array(512)
47 | @gradP = new Array(512)
48 | @seed(seed)
49 |
50 | # This isn't a very good seeding function, but it works ok. It supports 2^16
51 | # different seed values. Write something better if you need more seeds.
52 | seed : (seed) ->
53 | # Scale the seed out
54 | if(seed > 0 && seed < 1)
55 | seed *= 65536
56 |
57 | seed = Math.floor(seed)
58 | if(seed < 256)
59 | seed |= seed << 8
60 |
61 | for i in [0...256]
62 | v = 0
63 | if (i & 1)
64 | v = SIMPLEX_PERMUTATIONS_TABLE[i] ^ (seed & 255)
65 | else
66 | v = SIMPLEX_PERMUTATIONS_TABLE[i] ^ ((seed>>8) & 255)
67 |
68 | @perm[i] = @perm[i + 256] = v
69 | @gradP[i] = @gradP[i + 256] = grad3[v % 12]
70 |
71 | noise : (x, y, z) ->
72 | # Skew the input space to determine which simplex cell we're in
73 | s = (x + y + z)*F3 # Hairy factor for 2D
74 | i = Math.floor(x + s)
75 | j = Math.floor(y + s)
76 | k = Math.floor(z + s)
77 |
78 | t = (i + j + k) * G3
79 | x0 = x - i + t # The x,y distances from the cell origin, unskewed.
80 | y0 = y - j + t
81 | z0 = z - k + t
82 |
83 | # For the 3D case, the simplex shape is a slightly irregular tetrahedron.
84 | # Determine which simplex we are in.
85 | # Offsets for second corner of simplex in (i,j,k) coords
86 | # Offsets for third corner of simplex in (i,j,k) coords
87 | if(x0 >= y0)
88 | if(y0 >= z0)
89 | i1=1; j1=0; k1=0; i2=1; j2=1; k2=0;
90 | else if(x0 >= z0)
91 | i1=1; j1=0; k1=0; i2=1; j2=0; k2=1;
92 | else
93 | i1=0; j1=0; k1=1; i2=1; j2=0; k2=1;
94 | else
95 | if(y0 < z0)
96 | i1=0; j1=0; k1=1; i2=0; j2=1; k2=1;
97 | else if(x0 < z0)
98 | i1=0; j1=1; k1=0; i2=0; j2=1; k2=1;
99 | else
100 | i1=0; j1=1; k1=0; i2=1; j2=1; k2=0;
101 |
102 | # A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z),
103 | # a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and
104 | # a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where
105 | # c = 1/6.
106 | x1 = x0 - i1 + G3 # Offsets for second corner
107 | y1 = y0 - j1 + G3
108 | z1 = z0 - k1 + G3
109 |
110 | x2 = x0 - i2 + 2 * G3 # Offsets for third corner
111 | y2 = y0 - j2 + 2 * G3
112 | z2 = z0 - k2 + 2 * G3
113 |
114 | x3 = x0 - 1 + 3 * G3 # Offsets for fourth corner
115 | y3 = y0 - 1 + 3 * G3
116 | z3 = z0 - 1 + 3 * G3
117 |
118 | # Work out the hashed gradient indices of the four simplex corners
119 | i &= 0xFF
120 | j &= 0xFF
121 | k &= 0xFF
122 | gi0 = @gradP[i + @perm[j + @perm[k]]]
123 | gi1 = @gradP[i + i1 + @perm[j + j1 + @perm[k + k1]]]
124 | gi2 = @gradP[i + i2 + @perm[j + j2 + @perm[k + k2]]]
125 | gi3 = @gradP[i + 1 + @perm[j + 1 + @perm[k + 1]]]
126 |
127 | # Calculate the contribution from the four corners
128 | t0 = 0.5 - x0*x0-y0*y0-z0*z0
129 | if(t0<0)
130 | n0 = 0
131 | else
132 | t0 *= t0
133 | n0 = t0 * t0 * gi0.dot(x0, y0, z0) # (x,y) of grad3 used for 2D gradient
134 |
135 | t1 = 0.5 - x1*x1-y1*y1-z1*z1
136 | if(t1<0)
137 | n1 = 0
138 | else
139 | t1 *= t1
140 | n1 = t1 * t1 * gi1.dot(x1, y1, z1)
141 |
142 | t2 = 0.5 - x2*x2-y2*y2-z2*z2
143 | if(t2<0)
144 | n2 = 0
145 | else
146 | t2 *= t2
147 | n2 = t2 * t2 * gi2.dot(x2, y2, z2)
148 |
149 | t3 = 0.5 - x3*x3-y3*y3-z3*z3
150 | if(t3<0)
151 | n3 = 0
152 | else
153 | t3 *= t3
154 | n3 = t3 * t3 * gi3.dot(x3, y3, z3)
155 |
156 | # Add contributions from each corner to get the final noise value.
157 | # The result is scaled to return values in the interval [-1,1].
158 | return 32 * (n0 + n1 + n2 + n3)
159 |
--------------------------------------------------------------------------------
/src/light.coffee:
--------------------------------------------------------------------------------
1 | # ## Lights
2 | # ------------------
3 |
4 | # This model object holds the attributes and transformation of a light source.
5 | class seen.Light extends seen.Transformable
6 | defaults :
7 | point : seen.P()
8 | color : seen.Colors.white()
9 | intensity : 0.01
10 | normal : seen.P(1, -1, -1).normalize()
11 | enabled : true
12 |
13 | constructor: (@type, options) ->
14 | super
15 | seen.Util.defaults(@, options, @defaults)
16 | @id = seen.Util.uniqueId('l')
17 |
18 | render : ->
19 | @colorIntensity = @color.copy().scale(@intensity)
20 |
21 | seen.Lights = {
22 | # A point light emits light eminating in all directions from a single point.
23 | # The `point` property determines the location of the point light. Note,
24 | # though, that it may also be moved through the transformation of the light.
25 | point : (opts) -> new seen.Light 'point', opts
26 |
27 | # A directional lights emit light in parallel lines, not eminating from any
28 | # single point. For these lights, only the `normal` property is used to
29 | # determine the direction of the light. This may also be transformed.
30 | directional : (opts) -> new seen.Light 'directional', opts
31 |
32 | # Ambient lights emit a constant amount of light everywhere at once.
33 | # Transformation of the light has no effect.
34 | ambient : (opts) -> new seen.Light 'ambient', opts
35 | }
36 |
--------------------------------------------------------------------------------
/src/materials.coffee:
--------------------------------------------------------------------------------
1 | # ## Materials
2 | # #### Surface material properties
3 | # ------------------
4 |
5 |
6 | # `Material` objects hold the attributes that desribe the color and finish of a surface.
7 | class seen.Material
8 | @create : (value) ->
9 | if value instanceof seen.Material
10 | return value
11 | else if value instanceof seen.Color
12 | return new seen.Material(value)
13 | else if typeof value is 'string'
14 | return new seen.Material(seen.Colors.parse(value))
15 | else
16 | return new seen.Material()
17 |
18 | defaults :
19 | # The base color of the material.
20 | color : seen.Colors.gray()
21 |
22 | # The `metallic` attribute determines how the specular highlights are
23 | # calculated. Normally, specular highlights are the color of the light
24 | # source. If metallic is true, specular highlight colors are determined
25 | # from the `specularColor` attribute.
26 | metallic : false
27 |
28 | # The color used for specular highlights when `metallic` is true.
29 | specularColor : seen.Colors.white()
30 |
31 | # The `specularExponent` determines how "shiny" the material is. A low
32 | # exponent will create a low-intesity, diffuse specular shine. A high
33 | # exponent will create an intense, point-like specular shine.
34 | specularExponent : 15
35 |
36 | # A `Shader` object may be supplied to override the shader used for this
37 | # material. For example, if you want to apply a flat color to text or
38 | # other shapes, set this value to `seen.Shaders.Flat`.
39 | shader : null
40 |
41 | constructor : (@color, options = {}) ->
42 | seen.Util.defaults(@, options, @defaults)
43 |
44 | # Apply the shader's shading to this material, with the option to override
45 | # the shader with the material's shader (if defined).
46 | render : (lights, shader, renderData) ->
47 | renderShader = @shader ? shader
48 | color = renderShader.shade(lights, renderData, @)
49 | color.a = @color.a
50 | return color
51 |
--------------------------------------------------------------------------------
/src/matrix.coffee:
--------------------------------------------------------------------------------
1 |
2 | # ## Math
3 | # #### Matrices, points, and other mathy stuff
4 | # ------------------
5 |
6 | # Pool object to speed computation and reduce object creation
7 | ARRAY_POOL = new Array(16)
8 |
9 | # Definition of identity matrix values
10 | IDENTITY = [1, 0, 0, 0,
11 | 0, 1, 0, 0,
12 | 0, 0, 1, 0,
13 | 0, 0, 0, 1]
14 |
15 | # Indices with which to transpose the matrix array
16 | TRANSPOSE_INDICES = [0, 4, 8, 12,
17 | 1, 5, 9, 13,
18 | 2, 6, 10, 14,
19 | 3, 7, 11, 15]
20 |
21 |
22 |
23 | # The `Matrix` class stores transformations in the scene. These include:
24 | # (1) Camera Projection and Viewport transformations.
25 | # (2) Transformations of any `Transformable` type object, such as `Shape`s or `Model`s
26 | #
27 | # Most of the methods on `Matrix` are destructive, so be sure to use `.copy()`
28 | # when you want to preserve an object's value.
29 | class seen.Matrix
30 | # Accepts a 16-value `Array`, defaults to the identity matrix.
31 | constructor : (@m = null) ->
32 | @m ?= IDENTITY.slice()
33 | @baked = IDENTITY
34 |
35 | # Returns a new matrix instances with a copy of the value array
36 | copy : ->
37 | return new seen.Matrix(@m.slice())
38 |
39 | # Multiply by the 16-value `Array` argument. This method uses the
40 | # `ARRAY_POOL`, which prevents us from having to re-initialize a new
41 | # temporary matrix every time. This drastically improves performance.
42 | matrix : (m) ->
43 | c = ARRAY_POOL
44 | for j in [0...4]
45 | for i in [0...16] by 4
46 | c[i + j] =
47 | m[i ] * @m[ j] +
48 | m[i + 1] * @m[ 4 + j] +
49 | m[i + 2] * @m[ 8 + j] +
50 | m[i + 3] * @m[12 + j]
51 | ARRAY_POOL = @m
52 | @m = c
53 | return @
54 |
55 | # Resets the matrix to the baked-in (default: identity).
56 | reset : ->
57 | @m = @baked.slice()
58 | return @
59 |
60 | # Sets the array that this matrix will return to when calling `.reset()`.
61 | # With no arguments, it uses the current matrix state.
62 | bake : (m) ->
63 | @baked = (m ? @m).slice()
64 | return @
65 |
66 | # Multiply by the `Matrix` argument.
67 | multiply : (b) ->
68 | return @matrix(b.m)
69 |
70 | # Tranposes this matrix
71 | transpose : ->
72 | c = ARRAY_POOL
73 | for ti, i in TRANSPOSE_INDICES
74 | c[i] = @m[ti]
75 | ARRAY_POOL = @m
76 | @m = c
77 | return @
78 |
79 | # Apply a rotation about the X axis. `Theta` is measured in Radians
80 | rotx : (theta) ->
81 | ct = Math.cos(theta)
82 | st = Math.sin(theta)
83 | rm = [ 1, 0, 0, 0, 0, ct, -st, 0, 0, st, ct, 0, 0, 0, 0, 1 ]
84 | return @matrix(rm)
85 |
86 | # Apply a rotation about the Y axis. `Theta` is measured in Radians
87 | roty : (theta) ->
88 | ct = Math.cos(theta)
89 | st = Math.sin(theta)
90 | rm = [ ct, 0, st, 0, 0, 1, 0, 0, -st, 0, ct, 0, 0, 0, 0, 1 ]
91 | return @matrix(rm)
92 |
93 | # Apply a rotation about the Z axis. `Theta` is measured in Radians
94 | rotz : (theta) ->
95 | ct = Math.cos(theta)
96 | st = Math.sin(theta)
97 | rm = [ ct, -st, 0, 0, st, ct, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]
98 | return @matrix(rm)
99 |
100 | # Apply a translation. All arguments default to `0`
101 | translate : (x = 0, y = 0, z = 0) ->
102 | rm = [ 1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1 ]
103 | return @matrix(rm)
104 |
105 | # Apply a scale. If not all arguments are supplied, each dimension (x,y,z)
106 | # is copied from the previous arugment. Therefore, `_scale()` is equivalent
107 | # to `_scale(1,1,1)`, and `_scale(1,-1)` is equivalent to `_scale(1,-1,-1)`
108 | scale : (sx, sy, sz) ->
109 | sx ?= 1
110 | sy ?= sx
111 | sz ?= sy
112 | rm = [ sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1 ]
113 | return @matrix(rm)
114 |
115 |
116 | # A convenience method for constructing Matrix objects.
117 | seen.M = (m) -> new seen.Matrix(m)
118 |
119 | # A few useful Matrix objects.
120 | seen.Matrices = {
121 | identity : -> seen.M()
122 | flipX : -> seen.M().scale(-1, 1, 1)
123 | flipY : -> seen.M().scale( 1,-1, 1)
124 | flipZ : -> seen.M().scale( 1, 1,-1)
125 | }
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/src/model.coffee:
--------------------------------------------------------------------------------
1 | # ## Models
2 | # ------------------
3 |
4 | # The object model class. It stores `Shapes`, `Lights`, and other `Models` as
5 | # well as a transformation matrix.
6 | #
7 | # Notably, models are hierarchical, like a tree. This means you can isolate
8 | # the transformation of groups of shapes in the scene, as well as create
9 | # chains of transformations for creating, for example, articulated skeletons.
10 | class seen.Model extends seen.Transformable
11 | constructor: () ->
12 | super()
13 | @children = []
14 | @lights = []
15 |
16 | # Add a `Shape`, `Light`, and other `Model` as a child of this `Model`
17 | # Any number of children can by supplied as arguments.
18 | add: (childs...) ->
19 | for child in childs
20 | if child instanceof seen.Shape or child instanceof seen.Model
21 | @children.push child
22 | else if child instanceof seen.Light
23 | @lights.push child
24 | return @
25 |
26 | # Remove a shape, model, or light from the model. NOTE: the scene may still
27 | # contain a renderModel in its cache. If you are adding and removing many items,
28 | # consider calling `.flush()` on the scene to flush its renderModel cache.
29 | remove : (childs...) ->
30 | for child in childs
31 | while (i = @children.indexOf(child)) >= 0
32 | @children.splice(i,1)
33 | while (i = @lights.indexOf(child)) >= 0
34 | @lights.splice(i,1)
35 |
36 | # Create a new child model and return it.
37 | append: () ->
38 | model = new seen.Model
39 | @add model
40 | return model
41 |
42 | # Visit each `Shape` in this `Model` and all recursive child `Model`s.
43 | eachShape: (f) ->
44 | for child in @children
45 | if child instanceof seen.Shape
46 | f.call(@, child)
47 | if child instanceof seen.Model
48 | child.eachShape(f)
49 |
50 | # Visit each `Light` and `Shape`, accumulating the recursive transformation
51 | # matrices along the way. The light callback will be called with each light
52 | # and its accumulated transform and it should return a `LightModel`. Each
53 | # shape callback with be called with each shape and its accumulated
54 | # transform as well as the list of light models that apply to that shape.
55 | eachRenderable : (lightFn, shapeFn) ->
56 | @_eachRenderable(lightFn, shapeFn, [], @m)
57 |
58 | _eachRenderable : (lightFn, shapeFn, lightModels, transform) ->
59 | if @lights.length > 0 then lightModels = lightModels.slice()
60 | for light in @lights
61 | continue unless light.enabled
62 | lightModels.push lightFn.call(@, light, light.m.copy().multiply(transform))
63 |
64 | for child in @children
65 | if child instanceof seen.Shape
66 | shapeFn.call(@, child, lightModels, child.m.copy().multiply(transform))
67 | if child instanceof seen.Model
68 | child._eachRenderable(lightFn, shapeFn, lightModels, child.m.copy().multiply(transform))
69 |
70 | seen.Models = {
71 | # The default model contains standard Hollywood-style 3-part lighting
72 | default : ->
73 | model = new seen.Model()
74 |
75 | # Key light
76 | model.add seen.Lights.directional
77 | normal : seen.P(-1, 1, 1).normalize()
78 | color : seen.Colors.hsl(0.1, 0.3, 0.7)
79 | intensity : 0.004
80 |
81 | # Back light
82 | model.add seen.Lights.directional
83 | normal : seen.P(1, 1, -1).normalize()
84 | intensity : 0.003
85 |
86 | # Fill light
87 | model.add seen.Lights.ambient
88 | intensity : 0.0015
89 |
90 | return model
91 | }
92 |
--------------------------------------------------------------------------------
/src/namespace.coffee:
--------------------------------------------------------------------------------
1 | # ## Init
2 | # #### Module definition
3 | # ------------------
4 |
5 | # Declare and attach seen namespace
6 | seen = {}
7 | if window? then window.seen = seen # for the web
8 | if module?.exports? then module.exports = seen # for node
--------------------------------------------------------------------------------
/src/point.coffee:
--------------------------------------------------------------------------------
1 | # The `Point` object contains x,y,z, and w coordinates. `Point`s support
2 | # various arithmetic operations with other `Points`, scalars, or `Matrices`.
3 | #
4 | # Most of the methods on `Point` are destructive, so be sure to use `.copy()`
5 | # when you want to preserve an object's value.
6 | class seen.Point
7 | constructor : (@x = 0, @y = 0, @z = 0, @w = 1) ->
8 |
9 | # Creates and returns a new `Point` with the same values as this object.
10 | copy : () ->
11 | return new seen.Point(@x, @y, @z, @w)
12 |
13 | # Copies the values of the supplied `Point` into this object.
14 | set : (p) ->
15 | @x = p.x
16 | @y = p.y
17 | @z = p.z
18 | @w = p.w
19 | return @
20 |
21 | # Performs parameter-wise addition with the supplied `Point`. Excludes `@w`.
22 | add : (q) ->
23 | @x += q.x
24 | @y += q.y
25 | @z += q.z
26 | return @
27 |
28 | # Performs parameter-wise subtraction with the supplied `Point`. Excludes `@w`.
29 | subtract : (q) ->
30 | @x -= q.x
31 | @y -= q.y
32 | @z -= q.z
33 | return @
34 |
35 | # Apply a translation. Excludes `@w`.
36 | translate: (x, y, z) ->
37 | @x += x
38 | @y += y
39 | @z += z
40 | return @
41 |
42 | # Multiplies each parameters by the supplied scalar value. Excludes `@w`.
43 | multiply : (n) ->
44 | @x *= n
45 | @y *= n
46 | @z *= n
47 | return @
48 |
49 | # Divides each parameters by the supplied scalar value. Excludes `@w`.
50 | divide : (n) ->
51 | @x /= n
52 | @y /= n
53 | @z /= n
54 | return @
55 |
56 | # Rounds each coordinate to the nearest integer. Excludes `@w`.
57 | round : () ->
58 | @x = Math.round(@x)
59 | @y = Math.round(@y)
60 | @z = Math.round(@z)
61 | return @
62 |
63 | # Divides this `Point` by its magnitude. If the point is (0,0,0) we return (0,0,1).
64 | normalize : () ->
65 | n = @magnitude()
66 | if n == 0 # Strict zero comparison -- may be worth using an epsilon
67 | @set(seen.Points.Z())
68 | else
69 | @divide(n)
70 | return @
71 |
72 | # Returns a new point that is perpendicular to this point
73 | perpendicular : () ->
74 | n = @copy().cross(seen.Points.Z())
75 | mag = n.magnitude()
76 | if mag isnt 0 then return n.divide(mag)
77 | return @copy().cross(seen.Points.X()).normalize()
78 |
79 | # Apply a transformation from the supplied `Matrix`.
80 | transform : (matrix) ->
81 | r = POINT_POOL
82 | r.x = @x * matrix.m[0] + @y * matrix.m[1] + @z * matrix.m[2] + @w * matrix.m[3]
83 | r.y = @x * matrix.m[4] + @y * matrix.m[5] + @z * matrix.m[6] + @w * matrix.m[7]
84 | r.z = @x * matrix.m[8] + @y * matrix.m[9] + @z * matrix.m[10] + @w * matrix.m[11]
85 | r.w = @x * matrix.m[12] + @y * matrix.m[13] + @z * matrix.m[14] + @w * matrix.m[15]
86 |
87 | @set(r)
88 | return @
89 |
90 | # Returns this `Point`s magnitude squared. Excludes `@w`.
91 | magnitudeSquared : () ->
92 | return @dot(@)
93 |
94 | # Returns this `Point`s magnitude. Excludes `@w`.
95 | magnitude : () ->
96 | return Math.sqrt(@magnitudeSquared())
97 |
98 | # Computes the dot product with the supplied `Point`.
99 | dot : (q) ->
100 | return @x * q.x + @y * q.y + @z * q.z
101 |
102 | # Computes the cross product with the supplied `Point`.
103 | cross : (q) ->
104 | r = POINT_POOL
105 | r.x = @y * q.z - @z * q.y
106 | r.y = @z * q.x - @x * q.z
107 | r.z = @x * q.y - @y * q.x
108 |
109 | @set(r)
110 | return @
111 |
112 | # Convenience method for creating `Points`.
113 | seen.P = (x,y,z,w) -> new seen.Point(x,y,z,w)
114 |
115 | # A pool object which prevents us from having to create new `Point` objects
116 | # for various calculations, which vastly improves performance.
117 | POINT_POOL = seen.P()
118 |
119 | # A few useful `Point` objects. Be sure that you don't invoke destructive
120 | # methods on these objects.
121 | seen.Points = {
122 | X : -> seen.P(1, 0, 0)
123 | Y : -> seen.P(0, 1, 0)
124 | Z : -> seen.P(0, 0, 1)
125 | ZERO : -> seen.P(0, 0, 0)
126 | }
127 |
--------------------------------------------------------------------------------
/src/quaternion.coffee:
--------------------------------------------------------------------------------
1 | # A Quaterionion class for computing quaterion multiplications. This creates
2 | # more natural mouse rotations.
3 | #
4 | # Attribution: adapted from http://glprogramming.com/codedump/godecho/quaternion.html
5 | class seen.Quaternion
6 | @pixelsPerRadian : 150
7 |
8 | # Convert the x and y pixel offsets into a rotation matrix
9 | @xyToTransform : (x, y) ->
10 | quatX = seen.Quaternion.pointAngle(seen.Points.Y(), x / seen.Quaternion.pixelsPerRadian)
11 | quatY = seen.Quaternion.pointAngle(seen.Points.X(), y / seen.Quaternion.pixelsPerRadian)
12 | return quatX.multiply(quatY).toMatrix()
13 |
14 | # Create a rotation matrix from the axis defined by x, y, and z values, and the supplied angle.
15 | @axisAngle : (x, y, z, angleRads) ->
16 | scale = Math.sin(angleRads / 2.0)
17 | w = Math.cos(angleRads / 2.0)
18 | return new seen.Quaternion(scale * x, scale * y, scale * z, w)
19 |
20 | # Create a rotation matrix from the axis defined by the supplied point and the supplied angle.
21 | @pointAngle : (p, angleRads) ->
22 | scale = Math.sin(angleRads / 2.0)
23 | w = Math.cos(angleRads / 2.0)
24 | return new seen.Quaternion(scale * p.x, scale * p.y, scale * p.z, w)
25 |
26 | constructor : ->
27 | @q = seen.P(arguments...)
28 |
29 | # Multiply this `Quaterionion` by the `Quaternion` argument.
30 | multiply : (q) ->
31 | r = seen.P()
32 |
33 | r.w = @q.w * q.q.w - @q.x * q.q.x - @q.y * q.q.y - @q.z * q.q.z
34 | r.x = @q.w * q.q.x + @q.x * q.q.w + @q.y * q.q.z - @q.z * q.q.y
35 | r.y = @q.w * q.q.y + @q.y * q.q.w + @q.z * q.q.x - @q.x * q.q.z
36 | r.z = @q.w * q.q.z + @q.z * q.q.w + @q.x * q.q.y - @q.y * q.q.x
37 |
38 | result = new seen.Quaternion()
39 | result.q = r
40 | return result
41 |
42 | # Convert this `Quaterion` into a transformation matrix.
43 | toMatrix : ->
44 | m = new Array(16)
45 |
46 | m[ 0] = 1.0 - 2.0 * ( @q.y * @q.y + @q.z * @q.z )
47 | m[ 1] = 2.0 * ( @q.x * @q.y - @q.w * @q.z )
48 | m[ 2] = 2.0 * ( @q.x * @q.z + @q.w * @q.y )
49 | m[ 3] = 0.0
50 |
51 | m[ 4] = 2.0 * ( @q.x * @q.y + @q.w * @q.z )
52 | m[ 5] = 1.0 - 2.0 * ( @q.x * @q.x + @q.z * @q.z )
53 | m[ 6] = 2.0 * ( @q.y * @q.z - @q.w * @q.x )
54 | m[ 7] = 0.0
55 |
56 | m[ 8] = 2.0 * ( @q.x * @q.z - @q.w * @q.y )
57 | m[ 9] = 2.0 * ( @q.y * @q.z + @q.w * @q.x )
58 | m[10] = 1.0 - 2.0 * ( @q.x * @q.x + @q.y * @q.y )
59 | m[11] = 0.0
60 |
61 | m[12] = 0
62 | m[13] = 0
63 | m[14] = 0
64 | m[15] = 1.0
65 | return seen.M(m)
66 |
--------------------------------------------------------------------------------
/src/render/canvas.coffee:
--------------------------------------------------------------------------------
1 | # ## HTML5 Canvas Context
2 | # ------------------
3 |
4 | class seen.CanvasStyler
5 | constructor : (@ctx) ->
6 |
7 | draw : (style = {}) ->
8 | # Copy over SVG CSS attributes
9 | if style.stroke? then @ctx.strokeStyle = style.stroke
10 | if style['stroke-width']? then @ctx.lineWidth = style['stroke-width']
11 | if style['text-anchor']? then @ctx.textAlign = style['text-anchor']
12 |
13 | @ctx.stroke()
14 | return @
15 |
16 | fill : (style = {}) ->
17 | # Copy over SVG CSS attributes
18 | if style.fill? then @ctx.fillStyle = style.fill
19 | if style['text-anchor']? then @ctx.textAlign = style['text-anchor']
20 | if style['fill-opacity'] then @ctx.globalAlpha = style['fill-opacity']
21 |
22 | @ctx.fill()
23 | return @
24 |
25 | class seen.CanvasPathPainter extends seen.CanvasStyler
26 | path: (points) ->
27 | @ctx.beginPath()
28 |
29 | for p, i in points
30 | if i is 0
31 | @ctx.moveTo(p.x, p.y)
32 | else
33 | @ctx.lineTo(p.x, p.y)
34 |
35 | @ctx.closePath()
36 | return @
37 |
38 | class seen.CanvasRectPainter extends seen.CanvasStyler
39 | rect: (width, height) ->
40 | @ctx.rect(0, 0, width, height)
41 | return @
42 |
43 | class seen.CanvasCirclePainter extends seen.CanvasStyler
44 | circle: (center, radius) ->
45 | @ctx.beginPath()
46 | @ctx.arc(center.x, center.y, radius, 0, 2*Math.PI, true)
47 | return @
48 |
49 | class seen.CanvasTextPainter
50 | constructor : (@ctx) ->
51 |
52 | fillText : (m, text, style = {}) ->
53 | @ctx.save()
54 | @ctx.setTransform(m[0], m[3], -m[1], -m[4], m[2], m[5])
55 |
56 | if style.font? then @ctx.font = style.font
57 | if style.fill? then @ctx.fillStyle = style.fill
58 | if style['text-anchor']? then @ctx.textAlign = @_cssToCanvasAnchor(style['text-anchor'])
59 |
60 | @ctx.fillText(text, 0, 0)
61 | @ctx.restore()
62 | return @
63 |
64 | _cssToCanvasAnchor : (anchor) ->
65 | if anchor is 'middle' then return 'center'
66 | return anchor
67 |
68 | class seen.CanvasLayerRenderContext extends seen.RenderLayerContext
69 | constructor : (@ctx) ->
70 | @pathPainter = new seen.CanvasPathPainter(@ctx)
71 | @ciclePainter = new seen.CanvasCirclePainter(@ctx)
72 | @textPainter = new seen.CanvasTextPainter(@ctx)
73 | @rectPainter = new seen.CanvasRectPainter(@ctx)
74 |
75 | path : () -> @pathPainter
76 | rect : () -> @rectPainter
77 | circle : () -> @ciclePainter
78 | text : () -> @textPainter
79 |
80 | class seen.CanvasRenderContext extends seen.RenderContext
81 | constructor: (@el) ->
82 | super()
83 | @el = seen.Util.element(@el)
84 | @ctx = @el.getContext('2d')
85 |
86 | layer : (layer) ->
87 | @layers.push {
88 | layer : layer
89 | context : new seen.CanvasLayerRenderContext(@ctx)
90 | }
91 | return @
92 |
93 | reset : ->
94 | @ctx.setTransform(1, 0, 0, 1, 0, 0)
95 | @ctx.clearRect(0, 0, @el.width, @el.height)
96 |
97 | seen.CanvasContext = (elementId, scene) ->
98 | context = new seen.CanvasRenderContext(elementId)
99 | if scene? then context.sceneLayer(scene)
100 | return context
101 |
102 |
--------------------------------------------------------------------------------
/src/render/context.coffee:
--------------------------------------------------------------------------------
1 | # ## Render Contexts
2 | # ------------------
3 |
4 | # The `RenderContext` uses `RenderModel`s produced by the scene's render method to paint the shapes into an HTML element.
5 | # Since we support both SVG and Canvas painters, the `RenderContext` and `RenderLayerContext` define a common interface.
6 | class seen.RenderContext
7 | constructor: ->
8 | @layers = []
9 |
10 | render: () =>
11 | @reset()
12 | for layer in @layers
13 | layer.context.reset()
14 | layer.layer.render(layer.context)
15 | layer.context.cleanup()
16 | @cleanup()
17 | return @
18 |
19 | # Returns a new `Animator` with this context's render method pre-registered.
20 | animate : ->
21 | return new seen.RenderAnimator(@)
22 |
23 | # Add a new `RenderLayerContext` to this context. This allows us to easily stack paintable components such as
24 | # a fill backdrop, or even multiple scenes in one context.
25 | layer: (layer) ->
26 | @layers.push {
27 | layer : layer
28 | context : @
29 | }
30 | return @
31 |
32 | sceneLayer : (scene) ->
33 | @layer(new seen.SceneLayer(scene))
34 | return @
35 |
36 | reset : ->
37 | cleanup : ->
38 |
39 | # The `RenderLayerContext` defines the interface for producing painters that can paint various things into the current layer.
40 | class seen.RenderLayerContext
41 | path : -> # Return a path painter
42 | rect : -> # Return a rect painter
43 | circle : -> # Return a circle painter
44 | text : -> # Return a text painter
45 |
46 | reset : ->
47 | cleanup : ->
48 |
49 | # Create a render context for the element with the specified `elementId`. elementId
50 | # should correspond to either an SVG or Canvas element.
51 | seen.Context = (elementId, scene = null) ->
52 | tag = seen.Util.element(elementId)?.tagName.toUpperCase()
53 | context = switch tag
54 | when 'SVG', 'G' then new seen.SvgRenderContext(elementId)
55 | when 'CANVAS' then new seen.CanvasRenderContext(elementId)
56 | if context? and scene?
57 | context.sceneLayer(scene)
58 | return context
59 |
--------------------------------------------------------------------------------
/src/render/layers.coffee:
--------------------------------------------------------------------------------
1 | # ## Layers
2 | # ------------------
3 |
4 | class seen.RenderLayer
5 | render: (context) =>
6 |
7 | class seen.SceneLayer extends seen.RenderLayer
8 | constructor : (@scene) ->
9 |
10 | render : (context) =>
11 | for renderModel in @scene.render()
12 | renderModel.surface.painter.paint(renderModel, context)
13 |
14 | class seen.FillLayer extends seen.RenderLayer
15 | constructor : (@width = 500, @height = 500, @fill = '#EEE') ->
16 |
17 | render: (context) =>
18 | context.rect()
19 | .rect(@width, @height)
20 | .fill(fill : @fill)
21 |
--------------------------------------------------------------------------------
/src/render/model.coffee:
--------------------------------------------------------------------------------
1 | # ## RenderModels
2 | # ------------------
3 |
4 | DEFAULT_NORMAL = seen.Points.Z()
5 |
6 | # The `RenderModel` object contains the transformed and projected points as
7 | # well as various data needed to shade and paint a `Surface`.
8 | #
9 | # Once initialized, the object will have a constant memory footprint down to
10 | # `Number` primitives. Also, we compare each transform and projection to
11 | # prevent unnecessary re-computation.
12 | #
13 | # If you need to force a re-computation, mark the surface as 'dirty'.
14 | class seen.RenderModel
15 | constructor: (@surface, @transform, @projection, @viewport) ->
16 | @points = @surface.points
17 | @transformed = @_initRenderData()
18 | @projected = @_initRenderData()
19 | @_update()
20 |
21 | update: (transform, projection, viewport) ->
22 | 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)
23 | return
24 | else
25 | @transform = transform
26 | @projection = projection
27 | @viewport = viewport
28 | @_update()
29 |
30 | _update: () ->
31 | # Apply model transforms to surface points
32 | @_math(@transformed, @points, @transform, false)
33 | # Project into camera space
34 | cameraSpace = @transformed.points.map (p) => p.copy().transform(@projection)
35 | @inFrustrum = @_checkFrustrum(cameraSpace)
36 | # Project into screen space
37 | @_math(@projected, cameraSpace, @viewport, true)
38 | @surface.dirty = false
39 |
40 | _checkFrustrum : (points) ->
41 | for p in points
42 | return false if (p.z <= -2)
43 | return true
44 |
45 | _initRenderData: ->
46 | return {
47 | points : (p.copy() for p in @points)
48 | bounds : new seen.Bounds()
49 | barycenter : seen.P()
50 | normal : seen.P()
51 | v0 : seen.P()
52 | v1 : seen.P()
53 | }
54 |
55 | _math: (set, points, transform, applyClip = false) ->
56 | # Apply transform to points
57 | for p,i in points
58 | sp = set.points[i]
59 | sp.set(p).transform(transform)
60 | # Applying the clip is what ultimately scales the x and y coordinates in
61 | # a perpsective projection
62 | if applyClip then sp.divide(sp.w)
63 |
64 | # Compute barycenter, which is used in aligning shapes in the painters
65 | # algorithm
66 | set.barycenter.multiply(0)
67 | for p in set.points
68 | set.barycenter.add(p)
69 | set.barycenter.divide(set.points.length)
70 |
71 | # Compute the bounding box of the points
72 | set.bounds.reset()
73 | for p in set.points
74 | set.bounds.add(p)
75 |
76 | # Compute normal, which is used for backface culling (when enabled)
77 | if set.points.length < 2
78 | set.v0.set(DEFAULT_NORMAL)
79 | set.v1.set(DEFAULT_NORMAL)
80 | set.normal.set(DEFAULT_NORMAL)
81 | else
82 | set.v0.set(set.points[1]).subtract(set.points[0])
83 | set.v1.set(set.points[points.length - 1]).subtract(set.points[0])
84 | set.normal.set(set.v0).cross(set.v1).normalize()
85 |
86 | # The `LightRenderModel` stores pre-computed values necessary for shading
87 | # surfaces with the supplied `Light`.
88 | class seen.LightRenderModel
89 | constructor: (@light, transform) ->
90 | @colorIntensity = @light.color.copy().scale(@light.intensity)
91 | @type = @light.type
92 | @intensity = @light.intensity
93 | @point = @light.point.copy().transform(transform)
94 | origin = seen.Points.ZERO().transform(transform)
95 | @normal = @light.normal.copy().transform(transform).subtract(origin).normalize()
96 |
--------------------------------------------------------------------------------
/src/render/painters.coffee:
--------------------------------------------------------------------------------
1 |
2 | # ## Painters
3 | # #### Surface painters
4 | # ------------------
5 |
6 | # Each `Painter` overrides the paint method. It uses the supplied
7 | # `RenderLayerContext`'s builders to create and style the geometry on screen.
8 | class seen.Painter
9 | paint : (renderModel, context) ->
10 |
11 | class seen.PathPainter extends seen.Painter
12 | paint : (renderModel, context) ->
13 | painter = context.path().path(renderModel.projected.points)
14 |
15 | if renderModel.fill?
16 | painter.fill(
17 | fill : if not renderModel.fill? then 'none' else renderModel.fill.hex()
18 | 'fill-opacity' : if not renderModel.fill?.a? then 1.0 else (renderModel.fill.a / 255.0)
19 | )
20 |
21 | if renderModel.stroke?
22 | painter.draw(
23 | fill : 'none'
24 | stroke : if not renderModel.stroke? then 'none' else renderModel.stroke.hex()
25 | 'stroke-width' : renderModel.surface['stroke-width'] ? 1
26 | )
27 |
28 | class seen.TextPainter extends seen.Painter
29 | paint : (renderModel, context) ->
30 | style = {
31 | fill : if not renderModel.fill? then 'none' else renderModel.fill.hex()
32 | font : renderModel.surface.font
33 | 'text-anchor' : renderModel.surface.anchor ? 'middle'
34 | }
35 | xform = seen.Affine.solveForAffineTransform(renderModel.projected.points)
36 | context.text().fillText(xform, renderModel.surface.text, style)
37 |
38 | seen.Painters = {
39 | path : new seen.PathPainter()
40 | text : new seen.TextPainter()
41 | }
42 |
--------------------------------------------------------------------------------
/src/render/svg.coffee:
--------------------------------------------------------------------------------
1 | # ## SVG Context
2 | # ------------------
3 |
4 | # Creates a new SVG element in the SVG namespace.
5 | _svg = (name) ->
6 | return document.createElementNS('http://www.w3.org/2000/svg', name)
7 |
8 | class seen.SvgStyler
9 | _attributes : {}
10 | _svgTag : 'g'
11 |
12 | constructor : (@elementFactory) ->
13 |
14 | clear : () ->
15 | @_attributes = {}
16 | return @
17 |
18 | fill : (style = {}) ->
19 | @_paint(style)
20 | return @
21 |
22 | draw : (style = {}) ->
23 | @_paint(style)
24 | return @
25 |
26 | _paint : (style) ->
27 | el = @elementFactory(@_svgTag)
28 |
29 | str = ''
30 | for key, value of style
31 | str += "#{key}:#{value};"
32 | el.setAttribute('style', str)
33 |
34 | for key, value of @_attributes
35 | el.setAttribute(key, value)
36 | return el
37 |
38 | class seen.SvgPathPainter extends seen.SvgStyler
39 | _svgTag : 'path'
40 |
41 | path : (points) ->
42 | @_attributes.d = 'M' + points.map((p) -> "#{p.x} #{p.y}").join 'L'
43 | return @
44 |
45 | class seen.SvgTextPainter
46 | _svgTag : 'text'
47 |
48 | constructor : (@elementFactory) ->
49 |
50 | fillText : (m, text, style = {}) ->
51 | el = @elementFactory(@_svgTag)
52 | el.setAttribute('transform', "matrix(#{m[0]} #{m[3]} #{-m[1]} #{-m[4]} #{m[2]} #{m[5]})")
53 |
54 | str = ''
55 | for key, value of style
56 | if value? then str += "#{key}:#{value};"
57 | el.setAttribute('style', str)
58 |
59 | el.textContent = text
60 |
61 |
62 | class seen.SvgRectPainter extends seen.SvgStyler
63 | _svgTag : 'rect'
64 |
65 | rect : (width, height) ->
66 | @_attributes.width = width
67 | @_attributes.height = height
68 | return @
69 |
70 | class seen.SvgCirclePainter extends seen.SvgStyler
71 | _svgTag : 'circle'
72 |
73 | circle: (center, radius) ->
74 | @_attributes.cx = center.x
75 | @_attributes.cy = center.y
76 | @_attributes.r = radius
77 | return @
78 |
79 | class seen.SvgLayerRenderContext extends seen.RenderLayerContext
80 | constructor : (@group) ->
81 | @pathPainter = new seen.SvgPathPainter(@_elementFactory)
82 | @textPainter = new seen.SvgTextPainter(@_elementFactory)
83 | @circlePainter = new seen.SvgCirclePainter(@_elementFactory)
84 | @rectPainter = new seen.SvgRectPainter(@_elementFactory)
85 | @_i = 0
86 |
87 | path : () -> @pathPainter.clear()
88 | rect : () -> @rectPainter.clear()
89 | circle : () -> @circlePainter.clear()
90 | text : () -> @textPainter
91 |
92 | reset : ->
93 | @_i = 0
94 |
95 | cleanup : ->
96 | children = @group.childNodes
97 | while (@_i < children.length)
98 | children[@_i].setAttribute('style', 'display: none;')
99 | @_i++
100 |
101 | # Returns an element with tagname `type`.
102 | #
103 | # This method uses an internal iterator to add new elements as they are
104 | # drawn. If there is no child element at the current index, we append one
105 | # and return it. If an element exists at the current index and it is the
106 | # same type, we return that. If the element is a different type, we create
107 | # one and replace it then return it.
108 | _elementFactory : (type) =>
109 | children = @group.childNodes
110 | if @_i >= children.length
111 | path = _svg(type)
112 | @group.appendChild(path)
113 | @_i++
114 | return path
115 |
116 | current = children[@_i]
117 | if current.tagName is type
118 | @_i++
119 | return current
120 | else
121 | path = _svg(type)
122 | @group.replaceChild(path, current)
123 | @_i++
124 | return path
125 |
126 | class seen.SvgRenderContext extends seen.RenderContext
127 | constructor : (@svg) ->
128 | super()
129 | @svg = seen.Util.element(@svg)
130 |
131 | layer : (layer) ->
132 | @svg.appendChild(group = _svg('g'))
133 | @layers.push {
134 | layer : layer
135 | context : new seen.SvgLayerRenderContext(group)
136 | }
137 | return @
138 |
139 | seen.SvgContext = (elementId, scene) ->
140 | context = new seen.SvgRenderContext(elementId)
141 | if scene? then context.sceneLayer(scene)
142 | return context
143 |
--------------------------------------------------------------------------------
/src/scene.coffee:
--------------------------------------------------------------------------------
1 | # ## Scene
2 | # ------------------
3 |
4 | # A `Scene` is the main object for a view of a scene.
5 | class seen.Scene
6 | defaults: ->
7 | # The root model for the scene, which contains `Shape`s, `Light`s, and
8 | # other `Model`s
9 | model : new seen.Model()
10 |
11 | # The `Camera`, which defines the projection transformation. The default
12 | # projection is perspective.
13 | camera : new seen.Camera()
14 |
15 | # The `Viewport`, which defines the projection from shape-space to
16 | # projection-space then to screen-space. The default viewport is on a
17 | # space from (0,0,0) to (1,1,1). To map more naturally to pixels, create a
18 | # viewport with the same width/height as the DOM element.
19 | viewport : seen.Viewports.origin(1,1)
20 |
21 | # The scene's shader determines which lighting model is used.
22 | shader : seen.Shaders.phong()
23 |
24 | # The `cullBackfaces` boolean can be used to turn off backface-culling
25 | # for the whole scene. Beware, turning this off can slow down a scene's
26 | # rendering by a factor of 2. You can also turn off backface-culling for
27 | # individual surfaces with a boolean on those objects.
28 | cullBackfaces : true
29 |
30 | # The `fractionalPoints` boolean determines if we round the surface
31 | # coordinates to the nearest integer. Rounding the coordinates before
32 | # display speeds up path drawing especially when using an SVG context
33 | # since it cuts down on the length of path data. Anecdotally, my speedup
34 | # on a complex demo scene was 10 FPS. However, it does introduce a slight
35 | # jittering effect when animating.
36 | fractionalPoints : false
37 |
38 | # The `cache` boolean (default : true) enables a simple cache for
39 | # renderModels, which are generated for each surface in the scene. The
40 | # cache is a simple Object keyed by the surface's unique id. The cache has
41 | # no eviction policy. To flush the cache, call `.flushCache()`
42 | cache : true
43 |
44 | constructor: (options) ->
45 | seen.Util.defaults(@, options, @defaults())
46 | @_renderModelCache = {}
47 |
48 | # The primary method that produces the render models, which are then used
49 | # by the `RenderContext` to paint the scene.
50 | render : () =>
51 | # Compute the projection matrix including the viewport and camera
52 | # transformation matrices.
53 | projection = @camera.m.copy()
54 | .multiply(@viewport.prescale)
55 | .multiply(@camera.projection)
56 | viewport = @viewport.postscale
57 |
58 | renderModels = []
59 | @model.eachRenderable(
60 | (light, transform) ->
61 | # Compute light model data.
62 | new seen.LightRenderModel(light, transform)
63 |
64 | (shape, lights, transform) =>
65 | for surface in shape.surfaces
66 | # Compute transformed and projected geometry.
67 | renderModel = @_renderSurface(surface, transform, projection, viewport)
68 |
69 | # Test projected normal's z-coordinate for culling (if enabled).
70 | if (not @cullBackfaces or not surface.cullBackfaces or renderModel.projected.normal.z < 0) and renderModel.inFrustrum
71 | # Render fill and stroke using material and shader.
72 | renderModel.fill = surface.fillMaterial?.render(lights, @shader, renderModel.transformed)
73 | renderModel.stroke = surface.strokeMaterial?.render(lights, @shader, renderModel.transformed)
74 |
75 | # Round coordinates (if enabled)
76 | if @fractionalPoints isnt true
77 | p.round() for p in renderModel.projected.points
78 |
79 | renderModels.push renderModel
80 | )
81 |
82 | # Sort render models by projected z coordinate. This ensures that the surfaces
83 | # farthest from the eye are painted first. (Painter's Algorithm)
84 | renderModels.sort (a, b) ->
85 | return b.projected.barycenter.z - a.projected.barycenter.z
86 |
87 | return renderModels
88 |
89 | # Get or create the rendermodel for the given surface. If `@cache` is true, we cache these models
90 | # to reduce object creation and recomputation.
91 | _renderSurface : (surface, transform, projection, viewport) ->
92 | if not @cache
93 | return new seen.RenderModel(surface, transform, projection, viewport)
94 |
95 | renderModel = @_renderModelCache[surface.id]
96 | if not renderModel?
97 | renderModel = @_renderModelCache[surface.id] = new seen.RenderModel(surface, transform, projection, viewport)
98 | else
99 | renderModel.update(transform, projection, viewport)
100 | return renderModel
101 |
102 | # Removes all elements from the cache. This may be necessary if you add and
103 | # remove many shapes from the scene's models since this cache has no
104 | # eviction policy.
105 | flushCache : () =>
106 | @_renderModelCache = {}
107 |
108 |
--------------------------------------------------------------------------------
/src/shaders.coffee:
--------------------------------------------------------------------------------
1 | # ## Shaders
2 | # ------------------
3 |
4 | EYE_NORMAL = seen.Points.Z()
5 |
6 | # These shading functions compute the shading for a surface. To reduce code
7 | # duplication, we aggregate them in a utils object.
8 | seen.ShaderUtils = {
9 | applyDiffuse : (c, light, lightNormal, surfaceNormal, material) ->
10 | dot = lightNormal.dot(surfaceNormal)
11 |
12 | if (dot > 0)
13 | # Apply diffuse phong shading
14 | c.addChannels(light.colorIntensity.copy().scale(dot))
15 |
16 | applyDiffuseAndSpecular : (c, light, lightNormal, surfaceNormal, material) ->
17 | dot = lightNormal.dot(surfaceNormal)
18 |
19 | if (dot > 0)
20 | # Apply diffuse phong shading
21 | c.addChannels(light.colorIntensity.copy().scale(dot))
22 |
23 | # Compute and apply specular phong shading
24 | reflectionNormal = surfaceNormal.copy().multiply(dot * 2).subtract(lightNormal)
25 | specularIntensity = Math.pow(0.5 + reflectionNormal.dot(EYE_NORMAL), material.specularExponent)
26 | specularColor = material.specularColor.copy().scale(specularIntensity * light.intensity / 255.0)
27 | c.addChannels(specularColor)
28 |
29 | applyAmbient : (c, light) ->
30 | # Apply ambient shading
31 | c.addChannels(light.colorIntensity)
32 | }
33 |
34 | # The `Shader` class is the base class for all shader objects.
35 | class seen.Shader
36 | # Every `Shader` implementation must override the `shade` method.
37 | #
38 | # `lights` is an object containing the ambient, point, and directional light sources.
39 | # `renderModel` is an instance of `RenderModel` and contains the transformed and projected surface data.
40 | # `material` is an instance of `Material` and contains the color and other attributes for determining how light reflects off the surface.
41 | shade: (lights, renderModel, material) -> # Override this
42 |
43 | # The `Phong` shader implements the Phong shading model with a diffuse,
44 | # specular, and ambient term.
45 | #
46 | # See https://en.wikipedia.org/wiki/Phong_reflection_model for more information
47 | class Phong extends seen.Shader
48 | shade: (lights, renderModel, material) ->
49 | c = new seen.Color()
50 |
51 | for light in lights
52 | switch light.type
53 | when 'point'
54 | lightNormal = light.point.copy().subtract(renderModel.barycenter).normalize()
55 | seen.ShaderUtils.applyDiffuseAndSpecular(c, light, lightNormal, renderModel.normal, material)
56 | when 'directional'
57 | seen.ShaderUtils.applyDiffuseAndSpecular(c, light, light.normal, renderModel.normal, material)
58 | when 'ambient'
59 | seen.ShaderUtils.applyAmbient(c, light)
60 |
61 | c.multiplyChannels(material.color)
62 |
63 | if material.metallic
64 | c.minChannels(material.specularColor)
65 |
66 | c.clamp(0, 0xFF)
67 | return c
68 |
69 | # The `DiffusePhong` shader implements the Phong shading model with a diffuse
70 | # and ambient term (no specular).
71 | class DiffusePhong extends seen.Shader
72 | shade: (lights, renderModel, material) ->
73 | c = new seen.Color()
74 |
75 | for light in lights
76 | switch light.type
77 | when 'point'
78 | lightNormal = light.point.copy().subtract(renderModel.barycenter).normalize()
79 | seen.ShaderUtils.applyDiffuse(c, light, lightNormal, renderModel.normal, material)
80 | when 'directional'
81 | seen.ShaderUtils.applyDiffuse(c, light, light.normal, renderModel.normal, material)
82 | when 'ambient'
83 | seen.ShaderUtils.applyAmbient(c, light)
84 |
85 | c.multiplyChannels(material.color).clamp(0, 0xFF)
86 | return c
87 |
88 | # The `Ambient` shader colors surfaces from ambient light only.
89 | class Ambient extends seen.Shader
90 | shade: (lights, renderModel, material) ->
91 | c = new seen.Color()
92 |
93 | for light in lights
94 | switch light.type
95 | when 'ambient'
96 | seen.ShaderUtils.applyAmbient(c, light)
97 |
98 | c.multiplyChannels(material.color).clamp(0, 0xFF)
99 | return c
100 |
101 | # The `Flat` shader colors surfaces with the material color, disregarding all
102 | # light sources.
103 | class Flat extends seen.Shader
104 | shade: (lights, renderModel, material) ->
105 | return material.color
106 |
107 | seen.Shaders = {
108 | phong : -> new Phong()
109 | diffuse : -> new DiffusePhong()
110 | ambient : -> new Ambient()
111 | flat : -> new Flat()
112 | }
113 |
--------------------------------------------------------------------------------
/src/shapes/bvh-parser.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/src/shapes/bvh-parser.js
--------------------------------------------------------------------------------
/src/shapes/mocap.coffee:
--------------------------------------------------------------------------------
1 |
2 | class seen.MocapModel
3 | constructor : (@model, @frames, @frameDelay) ->
4 |
5 | applyFrameTransforms : (frameIndex) ->
6 | frame = @frames[frameIndex]
7 | for transform in frame
8 | transform.shape.reset().transform(transform.transform)
9 | return (frameIndex + 1) % @frames.length
10 |
11 |
12 | class seen.MocapAnimator extends seen.Animator
13 | constructor : (@mocap) ->
14 | super
15 | @frameIndex = 0
16 | @frameDelay = @mocap.frameDelay
17 | @onFrame(@renderFrame)
18 |
19 | renderFrame : =>
20 | @frameIndex = @mocap.applyFrameTransforms(@frameIndex)
21 |
22 |
23 | class seen.Mocap
24 | @DEFAULT_SHAPE_FACTORY : (joint, endpoint) ->
25 | return seen.Shapes.pipe(seen.P(), endpoint)
26 |
27 | @parse : (source) ->
28 | return new seen.Mocap(seen.BvhParser.parse(source))
29 |
30 | constructor : (@bvh) ->
31 |
32 | createMocapModel : (shapeFactory = seen.Mocap.DEFAULT_SHAPE_FACTORY) ->
33 | model = new seen.Model()
34 | joints = []
35 | @_attachJoint(model, @bvh.root, joints, shapeFactory)
36 | frames = @bvh.motion.frames.map (frame) => @_generateFrameTransforms(frame, joints)
37 | return new seen.MocapModel(model, frames, @bvh.motion.frameTime * 1000)
38 |
39 | _generateFrameTransforms : (frame, joints) ->
40 | fi = 0
41 | transforms = joints.map (joint) =>
42 |
43 | # Apply channel actions in reverse order
44 | m = seen.M()
45 | ai = joint.channels.length
46 | while ai > 0
47 | ai -= 1
48 | @_applyChannelTransform(joint.channels[ai], m, frame[fi + ai])
49 | fi += joint.channels.length
50 |
51 | # Include offset as final tranform
52 | m.multiply(joint.offset)
53 |
54 | return {
55 | shape : joint.shape
56 | transform : m
57 | }
58 |
59 | return transforms
60 |
61 | _applyChannelTransform : (channel, m, v) ->
62 | switch channel
63 | when 'Xposition' then m.translate(v, 0, 0)
64 | when 'Yposition' then m.translate(0, v, 0)
65 | when 'Zposition' then m.translate(0, 0, v)
66 | when 'Xrotation' then m.rotx(v * Math.PI / 180.0)
67 | when 'Yrotation' then m.roty(v * Math.PI / 180.0)
68 | when 'Zrotation' then m.rotz(v * Math.PI / 180.0)
69 | return m
70 |
71 | _attachJoint : (model, joint, joints, shapeFactory) ->
72 | # Save joint offset
73 | offset = seen.M().translate(
74 | joint.offset?.x
75 | joint.offset?.y
76 | joint.offset?.z
77 | )
78 | model.transform(offset)
79 |
80 | # Create channel actions
81 | if joint.channels?
82 | joints.push {
83 | shape : model
84 | offset : offset
85 | channels : joint.channels
86 | }
87 |
88 | if joint.joints?
89 | # Append a model to store the child shapes
90 | childShapes = model.append()
91 |
92 | for child in joint.joints
93 | # Generate the child shape with the supplied shape factory
94 | p = seen.P(child.offset?.x, child.offset?.y, child.offset?.z)
95 | childShapes.add(shapeFactory(joint, p))
96 |
97 | # Recurse with a new model for any child joints
98 | if child.type is 'JOINT' then @_attachJoint(childShapes.append(), child, joints, shapeFactory)
99 | return
100 |
--------------------------------------------------------------------------------
/src/shapes/obj.coffee:
--------------------------------------------------------------------------------
1 | # Parser for Wavefront .obj files
2 | #
3 | # Note: Wavefront .obj array indicies are 1-based.
4 | class seen.ObjParser
5 | constructor : () ->
6 | @vertices = []
7 | @faces = []
8 | @commands =
9 | v : (data) => @vertices.push data.map (d) -> parseFloat(d)
10 | f : (data) => @faces.push data.map (d) -> parseInt(d)
11 |
12 | parse : (contents) ->
13 | for line in contents.split(/[\r\n]+/)
14 | data = line.trim().split(/[ ]+/)
15 |
16 | continue if data.length < 2 # Check line parsing
17 |
18 | command = data.slice(0,1)[0]
19 | data = data.slice(1)
20 |
21 | if command.charAt(0) is '#' # Check for comments
22 | continue
23 | if not @commands[command]? # Check that we know how the handle this command
24 | console.log "OBJ Parser: Skipping unknown command '#{command}'"
25 | continue
26 |
27 | @commands[command](data) # Execute command
28 |
29 | mapFacePoints : (faceMap) ->
30 | @faces.map (face) =>
31 | points = face.map (v) => seen.P(@vertices[v - 1]...)
32 | return faceMap.call(@, points)
33 |
34 | # This method accepts Wavefront .obj file content and returns a `Shape` object.
35 | seen.Shapes.obj = (objContents, cullBackfaces = true) ->
36 | parser = new seen.ObjParser()
37 | parser.parse(objContents)
38 | return new seen.Shape('obj', parser.mapFacePoints((points) ->
39 | surface = new seen.Surface points
40 | surface.cullBackfaces = cullBackfaces
41 | return surface
42 | ))
43 |
--------------------------------------------------------------------------------
/src/surface.coffee:
--------------------------------------------------------------------------------
1 | # ## Surfaces and Shapes
2 | # ------------------
3 |
4 | # A `Surface` is a defined as a planar object in 3D space. These paths don't
5 | # necessarily need to be convex, but they should be non-degenerate. This
6 | # library does not support shapes with holes.
7 | class seen.Surface
8 | # When 'false' this will override backface culling, which is useful if your
9 | # material is transparent. See comment in `seen.Scene`.
10 | cullBackfaces : true
11 |
12 | # Fill and stroke may be `Material` objects, which define the color and
13 | # finish of the object and are rendered using the scene's shader.
14 | fillMaterial : new seen.Material(seen.C.gray)
15 | strokeMaterial : null
16 |
17 | constructor : (@points, @painter = seen.Painters.path) ->
18 | # We store a unique id for every surface so we can look them up quickly
19 | # with the `renderModel` cache.
20 | @id = 's' + seen.Util.uniqueId()
21 |
22 | fill : (fill) ->
23 | @fillMaterial = seen.Material.create(fill)
24 | return @
25 |
26 | stroke : (stroke) ->
27 | @strokeMaterial = seen.Material.create(stroke)
28 | return @
29 |
30 | # A `Shape` contains a collection of surface. They may create a closed 3D
31 | # shape, but not necessarily. For example, a cube is a closed shape, but a
32 | # patch is not.
33 | class seen.Shape extends seen.Transformable
34 | constructor : (@type, @surfaces) ->
35 | super()
36 |
37 | # Visit each surface
38 | eachSurface: (f) ->
39 | @surfaces.forEach(f)
40 | return @
41 |
42 | # Apply the supplied fill `Material` to each surface
43 | fill : (fill) ->
44 | @eachSurface (s) -> s.fill(fill)
45 | return @
46 |
47 | # Apply the supplied stroke `Material` to each surface
48 | stroke : (stroke) ->
49 | @eachSurface (s) -> s.stroke(stroke)
50 | return @
51 |
--------------------------------------------------------------------------------
/src/transformable.coffee:
--------------------------------------------------------------------------------
1 |
2 | # `Transformable` base class extended by `Shape` and `Model`.
3 | #
4 | # The advantages of keeping transforms in `Matrix` form are (1) lazy
5 | # computation of point position (2) ability combine hierarchical
6 | # transformations easily (3) ability to reset transformations to an original
7 | # state.
8 | #
9 | # Resetting transformations is especially useful when you want to animate
10 | # interpolated values. Instead of computing the difference at each animation
11 | # step, you can compute the global interpolated value for that time step and
12 | # apply that value directly to a matrix (once it is reset).
13 | class seen.Transformable
14 | constructor: ->
15 | @m = new seen.Matrix()
16 | @baked = IDENTITY
17 |
18 | # We create shims for all of the matrix transformation methods so they
19 | # have the same interface.
20 | for method in ['scale', 'translate', 'rotx', 'roty', 'rotz', 'matrix', 'reset', 'bake'] then do (method) =>
21 | @[method] = ->
22 | @m[method].call(@m, arguments...)
23 | return @
24 |
25 | # Apply a transformation from the supplied `Matrix`. see `Matrix.multiply`
26 | transform: (m) ->
27 | @m.multiply(m)
28 | return @
29 |
--------------------------------------------------------------------------------
/src/util.coffee:
--------------------------------------------------------------------------------
1 | # ## Util
2 | # #### Utility methods
3 | # ------------------
4 |
5 | NEXT_UNIQUE_ID = 1 # An auto-incremented value
6 |
7 | seen.Util = {
8 | # Copies default values. First, overwrite undefined attributes of `obj` from
9 | # `opts`. Second, overwrite undefined attributes of `obj` from `defaults`.
10 | defaults: (obj, opts, defaults) ->
11 | for prop of opts
12 | if not obj[prop]? then obj[prop] = opts[prop]
13 | for prop of defaults
14 | if not obj[prop]? then obj[prop] = defaults[prop]
15 |
16 | # Returns `true` iff the supplied `Arrays` are the same size and contain the
17 | # same values.
18 | arraysEqual: (a, b) ->
19 | if not a.length == b.length then return false
20 | for val, i in a
21 | if not (val == b[i]) then return false
22 | return true
23 |
24 | # Returns an ID which is unique to this instance of the library
25 | uniqueId: (prefix = '') ->
26 | return prefix + NEXT_UNIQUE_ID++
27 |
28 | # Accept a DOM element or a string. If a string is provided, we assume it is
29 | # the id of an element, which we return.
30 | element : (elementOrString) ->
31 | if typeof elementOrString is 'string'
32 | return document.getElementById(elementOrString)
33 | else
34 | return elementOrString
35 | }
36 |
--------------------------------------------------------------------------------
/test/dev-site/dev.coffee:
--------------------------------------------------------------------------------
1 | module.exports = ->
2 | shape = new seen.Shape('tri', [new seen.Surface([
3 | seen.P(-1, -1, 0)
4 | seen.P( 1, -1, 0)
5 | seen.P( 0, 1, 0)
6 | ])]).scale(height * 0.2)
7 | shape.fill(new seen.Material(seen.Colors.gray()))
8 |
9 |
10 | #shape = seen.Shapes.sphere(2).scale(height * 0.4)
11 | #seen.Colors.randomSurfaces2(shape)
12 |
13 |
14 | model = new seen.Model().add(shape)
15 |
16 | model.add seen.Lights.directional
17 | normal : seen.P(-1, 1, 1).normalize()
18 | color : seen.Colors.hex('#FF0000')
19 | intensity : 0.01
20 |
21 | model.add seen.Lights.directional
22 | normal : seen.P(1, 1, -1).normalize()
23 | color : seen.Colors.hex('#0000FF')
24 | intensity : 0.01
25 |
26 | scene = new seen.Scene
27 | model : model
28 | viewport : seen.Viewports.center(width, height)
29 | context = seen.Context('seen-canvas', scene).render()
30 | dragger = new seen.Drag('seen-canvas', {inertia : true})
31 | dragger.on('drag.rotate', (e) ->
32 | shape.transform seen.Quaternion.xyToTransform(e.offsetRelative...)
33 | context.render()
34 | )
35 |
--------------------------------------------------------------------------------
/test/dev-site/index.coffee:
--------------------------------------------------------------------------------
1 | express = require 'express'
2 | path = require 'path'
3 | fs = require 'fs'
4 | options = require '../../site/options'
5 |
6 | app = express()
7 |
8 | app.configure ->
9 | app.engine('html', require('swig').renderFile)
10 | app.set('view engine', 'html')
11 | app.set('views', path.join(__dirname))
12 |
13 | app.use '/lib', express.static(path.join(__dirname, '..' , '..', 'dist', 'latest'))
14 |
15 | app.get '/', (req,res) ->
16 | res.render 'template', {
17 | scripts : [
18 | 'lib/seen.min.js'
19 | options.cdns.lodash.script
20 | options.cdns.jquery.script
21 | ]
22 | width : 800
23 | height : 800
24 | ready : String(require './dev')
25 | }
26 |
27 | server = app.listen 5050, ->
28 | console.log('Listening on port %d', server.address().port)
29 |
30 | process.once 'SIGUSR2', ->
31 | console.log 'Received SIGUSR2, closing server'
32 | server.close()
33 | setTimeout((-> process.kill(process.pid, 'SIGUSR2')), 1000)
34 |
--------------------------------------------------------------------------------
/test/dev-site/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |