├── .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 | <%= title %> 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 54 |
55 | 56 | -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /site/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /site/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #da532c 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /site/favicons/favicon-160x160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/favicon-160x160.png -------------------------------------------------------------------------------- /site/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /site/favicons/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/favicon-196x196.png -------------------------------------------------------------------------------- /site/favicons/favicon-260x260.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/favicon-260x260.png -------------------------------------------------------------------------------- /site/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /site/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /site/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/favicon.ico -------------------------------------------------------------------------------- /site/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /site/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /site/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /site/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /site/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/site/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /site/index.coffee: -------------------------------------------------------------------------------- 1 | express = require 'express' 2 | path = require 'path' 3 | 4 | app = express() 5 | app.use '/', express.static(path.join(__dirname, '..', 'site-dist')) 6 | server = app.listen 5000, -> console.log('Listening on port %d', server.address().port) 7 | 8 | process.once 'SIGUSR2', -> 9 | console.log 'Received SIGUSR2, closing server' 10 | server.close() 11 | setTimeout((-> process.kill(process.pid, 'SIGUSR2')), 1000) 12 | -------------------------------------------------------------------------------- /site/lib/2048/animframe_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var lastTime = 0; 3 | var vendors = ['webkit', 'moz']; 4 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 5 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 6 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || 7 | window[vendors[x] + 'CancelRequestAnimationFrame']; 8 | } 9 | 10 | if (!window.requestAnimationFrame) { 11 | window.requestAnimationFrame = function (callback) { 12 | var currTime = new Date().getTime(); 13 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 14 | var id = window.setTimeout(function () { 15 | callback(currTime + timeToCall); 16 | }, 17 | timeToCall); 18 | lastTime = currTime + timeToCall; 19 | return id; 20 | }; 21 | } 22 | 23 | if (!window.cancelAnimationFrame) { 24 | window.cancelAnimationFrame = function (id) { 25 | clearTimeout(id); 26 | }; 27 | } 28 | }()); 29 | -------------------------------------------------------------------------------- /site/lib/2048/application.js: -------------------------------------------------------------------------------- 1 | // Wait till the browser is ready to render the game (avoids glitches) 2 | window.requestAnimationFrame(function () { 3 | new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager); 4 | }); 5 | -------------------------------------------------------------------------------- /site/lib/2048/bind_polyfill.js: -------------------------------------------------------------------------------- 1 | Function.prototype.bind = Function.prototype.bind || function (target) { 2 | var self = this; 3 | return function (args) { 4 | if (!(args instanceof Array)) { 5 | args = [args]; 6 | } 7 | self.apply(target, args); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /site/lib/2048/classlist_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | if (typeof window.Element === "undefined" || 3 | "classList" in document.documentElement) { 4 | return; 5 | } 6 | 7 | var prototype = Array.prototype, 8 | push = prototype.push, 9 | splice = prototype.splice, 10 | join = prototype.join; 11 | 12 | function DOMTokenList(el) { 13 | this.el = el; 14 | // The className needs to be trimmed and split on whitespace 15 | // to retrieve a list of classes. 16 | var classes = el.className.replace(/^\s+|\s+$/g, '').split(/\s+/); 17 | for (var i = 0; i < classes.length; i++) { 18 | push.call(this, classes[i]); 19 | } 20 | } 21 | 22 | DOMTokenList.prototype = { 23 | add: function (token) { 24 | if (this.contains(token)) return; 25 | push.call(this, token); 26 | this.el.className = this.toString(); 27 | }, 28 | contains: function (token) { 29 | return this.el.className.indexOf(token) != -1; 30 | }, 31 | item: function (index) { 32 | return this[index] || null; 33 | }, 34 | remove: function (token) { 35 | if (!this.contains(token)) return; 36 | for (var i = 0; i < this.length; i++) { 37 | if (this[i] == token) break; 38 | } 39 | splice.call(this, i, 1); 40 | this.el.className = this.toString(); 41 | }, 42 | toString: function () { 43 | return join.call(this, ' '); 44 | }, 45 | toggle: function (token) { 46 | if (!this.contains(token)) { 47 | this.add(token); 48 | } else { 49 | this.remove(token); 50 | } 51 | 52 | return this.contains(token); 53 | } 54 | }; 55 | 56 | window.DOMTokenList = DOMTokenList; 57 | 58 | function defineElementGetter(obj, prop, getter) { 59 | if (Object.defineProperty) { 60 | Object.defineProperty(obj, prop, { 61 | get: getter 62 | }); 63 | } else { 64 | obj.__defineGetter__(prop, getter); 65 | } 66 | } 67 | 68 | defineElementGetter(HTMLElement.prototype, 'classList', function () { 69 | return new DOMTokenList(this); 70 | }); 71 | })(); 72 | -------------------------------------------------------------------------------- /site/lib/2048/grid.js: -------------------------------------------------------------------------------- 1 | function Grid(size, previousState) { 2 | this.size = size; 3 | this.cells = previousState ? this.fromState(previousState) : this.empty(); 4 | } 5 | 6 | // Build a grid of the specified size 7 | Grid.prototype.empty = function () { 8 | var cells = []; 9 | 10 | for (var x = 0; x < this.size; x++) { 11 | var row = cells[x] = []; 12 | 13 | for (var y = 0; y < this.size; y++) { 14 | row.push(null); 15 | } 16 | } 17 | 18 | return cells; 19 | }; 20 | 21 | Grid.prototype.fromState = function (state) { 22 | var cells = []; 23 | 24 | for (var x = 0; x < this.size; x++) { 25 | var row = cells[x] = []; 26 | 27 | for (var y = 0; y < this.size; y++) { 28 | var tile = state[x][y]; 29 | row.push(tile ? new Tile(tile.position, tile.value) : null); 30 | } 31 | } 32 | 33 | return cells; 34 | }; 35 | 36 | // Find the first available random position 37 | Grid.prototype.randomAvailableCell = function () { 38 | var cells = this.availableCells(); 39 | 40 | if (cells.length) { 41 | return cells[Math.floor(Math.random() * cells.length)]; 42 | } 43 | }; 44 | 45 | Grid.prototype.availableCells = function () { 46 | var cells = []; 47 | 48 | this.eachCell(function (x, y, tile) { 49 | if (!tile) { 50 | cells.push({ x: x, y: y }); 51 | } 52 | }); 53 | 54 | return cells; 55 | }; 56 | 57 | // Call callback for every cell 58 | Grid.prototype.eachCell = function (callback) { 59 | for (var x = 0; x < this.size; x++) { 60 | for (var y = 0; y < this.size; y++) { 61 | callback(x, y, this.cells[x][y]); 62 | } 63 | } 64 | }; 65 | 66 | // Check if there are any cells available 67 | Grid.prototype.cellsAvailable = function () { 68 | return !!this.availableCells().length; 69 | }; 70 | 71 | // Check if the specified cell is taken 72 | Grid.prototype.cellAvailable = function (cell) { 73 | return !this.cellOccupied(cell); 74 | }; 75 | 76 | Grid.prototype.cellOccupied = function (cell) { 77 | return !!this.cellContent(cell); 78 | }; 79 | 80 | Grid.prototype.cellContent = function (cell) { 81 | if (this.withinBounds(cell)) { 82 | return this.cells[cell.x][cell.y]; 83 | } else { 84 | return null; 85 | } 86 | }; 87 | 88 | // Inserts a tile at its position 89 | Grid.prototype.insertTile = function (tile) { 90 | this.cells[tile.x][tile.y] = tile; 91 | }; 92 | 93 | Grid.prototype.removeTile = function (tile) { 94 | this.cells[tile.x][tile.y] = null; 95 | }; 96 | 97 | Grid.prototype.withinBounds = function (position) { 98 | return position.x >= 0 && position.x < this.size && 99 | position.y >= 0 && position.y < this.size; 100 | }; 101 | 102 | Grid.prototype.serialize = function () { 103 | var cellState = []; 104 | 105 | for (var x = 0; x < this.size; x++) { 106 | var row = cellState[x] = []; 107 | 108 | for (var y = 0; y < this.size; y++) { 109 | row.push(this.cells[x][y] ? this.cells[x][y].serialize() : null); 110 | } 111 | } 112 | 113 | return { 114 | size: this.size, 115 | cells: cellState 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /site/lib/2048/html_actuator.js: -------------------------------------------------------------------------------- 1 | function HTMLActuator() { 2 | this.tileContainer = document.querySelector(".tile-container"); 3 | this.scoreContainer = document.querySelector(".score-container"); 4 | this.bestContainer = document.querySelector(".best-container"); 5 | this.messageContainer = document.querySelector(".game-message"); 6 | 7 | this.score = 0; 8 | } 9 | 10 | HTMLActuator.prototype.actuate = function (grid, metadata) { 11 | var self = this; 12 | 13 | window.requestAnimationFrame(function () { 14 | self.clearContainer(self.tileContainer); 15 | 16 | grid.cells.forEach(function (column) { 17 | column.forEach(function (cell) { 18 | if (cell) { 19 | self.addTile(cell); 20 | } 21 | }); 22 | }); 23 | 24 | self.updateScore(metadata.score); 25 | self.updateBestScore(metadata.bestScore); 26 | 27 | if (metadata.terminated) { 28 | if (metadata.over) { 29 | self.message(false); // You lose 30 | } else if (metadata.won) { 31 | self.message(true); // You win! 32 | } 33 | } 34 | 35 | }); 36 | }; 37 | 38 | // Continues the game (both restart and keep playing) 39 | HTMLActuator.prototype.continueGame = function () { 40 | this.clearMessage(); 41 | }; 42 | 43 | HTMLActuator.prototype.clearContainer = function (container) { 44 | while (container.firstChild) { 45 | container.removeChild(container.firstChild); 46 | } 47 | }; 48 | 49 | HTMLActuator.prototype.addTile = function (tile) { 50 | var self = this; 51 | 52 | var wrapper = document.createElement("div"); 53 | var inner = document.createElement("div"); 54 | var position = tile.previousPosition || { x: tile.x, y: tile.y }; 55 | var positionClass = this.positionClass(position); 56 | 57 | // We can't use classlist because it somehow glitches when replacing classes 58 | var classes = ["tile", "tile-" + tile.value, positionClass]; 59 | 60 | if (tile.value > 2048) classes.push("tile-super"); 61 | 62 | this.applyClasses(wrapper, classes); 63 | 64 | inner.classList.add("tile-inner"); 65 | inner.textContent = tile.value; 66 | 67 | if (tile.previousPosition) { 68 | // Make sure that the tile gets rendered in the previous position first 69 | window.requestAnimationFrame(function () { 70 | classes[2] = self.positionClass({ x: tile.x, y: tile.y }); 71 | self.applyClasses(wrapper, classes); // Update the position 72 | }); 73 | } else if (tile.mergedFrom) { 74 | classes.push("tile-merged"); 75 | this.applyClasses(wrapper, classes); 76 | 77 | // Render the tiles that merged 78 | tile.mergedFrom.forEach(function (merged) { 79 | self.addTile(merged); 80 | }); 81 | } else { 82 | classes.push("tile-new"); 83 | this.applyClasses(wrapper, classes); 84 | } 85 | 86 | // Add the inner part of the tile to the wrapper 87 | wrapper.appendChild(inner); 88 | 89 | // Put the tile on the board 90 | this.tileContainer.appendChild(wrapper); 91 | }; 92 | 93 | HTMLActuator.prototype.applyClasses = function (element, classes) { 94 | element.setAttribute("class", classes.join(" ")); 95 | }; 96 | 97 | HTMLActuator.prototype.normalizePosition = function (position) { 98 | return { x: position.x + 1, y: position.y + 1 }; 99 | }; 100 | 101 | HTMLActuator.prototype.positionClass = function (position) { 102 | position = this.normalizePosition(position); 103 | return "tile-position-" + position.x + "-" + position.y; 104 | }; 105 | 106 | HTMLActuator.prototype.updateScore = function (score) { 107 | this.clearContainer(this.scoreContainer); 108 | 109 | var difference = score - this.score; 110 | this.score = score; 111 | 112 | this.scoreContainer.textContent = this.score; 113 | 114 | if (difference > 0) { 115 | var addition = document.createElement("div"); 116 | addition.classList.add("score-addition"); 117 | addition.textContent = "+" + difference; 118 | 119 | this.scoreContainer.appendChild(addition); 120 | } 121 | }; 122 | 123 | HTMLActuator.prototype.updateBestScore = function (bestScore) { 124 | this.bestContainer.textContent = bestScore; 125 | }; 126 | 127 | HTMLActuator.prototype.message = function (won) { 128 | var type = won ? "game-won" : "game-over"; 129 | var message = won ? "You win!" : "Game over!"; 130 | 131 | this.messageContainer.classList.add(type); 132 | this.messageContainer.getElementsByTagName("p")[0].textContent = message; 133 | }; 134 | 135 | HTMLActuator.prototype.clearMessage = function () { 136 | // IE only takes one value to remove at a time. 137 | this.messageContainer.classList.remove("game-won"); 138 | this.messageContainer.classList.remove("game-over"); 139 | }; 140 | -------------------------------------------------------------------------------- /site/lib/2048/keyboard_input_manager.js: -------------------------------------------------------------------------------- 1 | function KeyboardInputManager() { 2 | this.events = {}; 3 | 4 | if (window.navigator.msPointerEnabled) { 5 | //Internet Explorer 10 style 6 | this.eventTouchstart = "MSPointerDown"; 7 | this.eventTouchmove = "MSPointerMove"; 8 | this.eventTouchend = "MSPointerUp"; 9 | } else { 10 | this.eventTouchstart = "touchstart"; 11 | this.eventTouchmove = "touchmove"; 12 | this.eventTouchend = "touchend"; 13 | } 14 | 15 | this.listen(); 16 | } 17 | 18 | KeyboardInputManager.prototype.on = function (event, callback) { 19 | if (!this.events[event]) { 20 | this.events[event] = []; 21 | } 22 | this.events[event].push(callback); 23 | }; 24 | 25 | KeyboardInputManager.prototype.emit = function (event, data) { 26 | var callbacks = this.events[event]; 27 | if (callbacks) { 28 | callbacks.forEach(function (callback) { 29 | callback(data); 30 | }); 31 | } 32 | }; 33 | 34 | KeyboardInputManager.prototype.listen = function () { 35 | var self = this; 36 | 37 | var map = { 38 | 38: 0, // Up 39 | 39: 1, // Right 40 | 40: 2, // Down 41 | 37: 3, // Left 42 | 75: 0, // Vim up 43 | 76: 1, // Vim right 44 | 74: 2, // Vim down 45 | 72: 3, // Vim left 46 | 87: 0, // W 47 | 68: 1, // D 48 | 83: 2, // S 49 | 65: 3 // A 50 | }; 51 | 52 | // Respond to direction keys 53 | document.addEventListener("keydown", function (event) { 54 | var modifiers = event.altKey || event.ctrlKey || event.metaKey || 55 | event.shiftKey; 56 | var mapped = map[event.which]; 57 | 58 | if (!modifiers) { 59 | if (mapped !== undefined) { 60 | event.preventDefault(); 61 | self.emit("move", mapped); 62 | } 63 | } 64 | 65 | // R key restarts the game 66 | if (!modifiers && event.which === 82) { 67 | self.restart.call(self, event); 68 | } 69 | }); 70 | 71 | // Respond to button presses 72 | this.bindButtonPress(".retry-button", this.restart); 73 | this.bindButtonPress(".restart-button", this.restart); 74 | this.bindButtonPress(".keep-playing-button", this.keepPlaying); 75 | 76 | // Respond to swipe events 77 | var touchStartClientX, touchStartClientY; 78 | var gameContainer = document.getElementsByClassName("game-container")[0]; 79 | 80 | gameContainer.addEventListener(this.eventTouchstart, function (event) { 81 | if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || 82 | event.targetTouches > 1) { 83 | return; // Ignore if touching with more than 1 finger 84 | } 85 | 86 | if (window.navigator.msPointerEnabled) { 87 | touchStartClientX = event.pageX; 88 | touchStartClientY = event.pageY; 89 | } else { 90 | touchStartClientX = event.touches[0].clientX; 91 | touchStartClientY = event.touches[0].clientY; 92 | } 93 | 94 | event.preventDefault(); 95 | }); 96 | 97 | gameContainer.addEventListener(this.eventTouchmove, function (event) { 98 | event.preventDefault(); 99 | }); 100 | 101 | gameContainer.addEventListener(this.eventTouchend, function (event) { 102 | if ((!window.navigator.msPointerEnabled && event.touches.length > 0) || 103 | event.targetTouches > 0) { 104 | return; // Ignore if still touching with one or more fingers 105 | } 106 | 107 | var touchEndClientX, touchEndClientY; 108 | 109 | if (window.navigator.msPointerEnabled) { 110 | touchEndClientX = event.pageX; 111 | touchEndClientY = event.pageY; 112 | } else { 113 | touchEndClientX = event.changedTouches[0].clientX; 114 | touchEndClientY = event.changedTouches[0].clientY; 115 | } 116 | 117 | var dx = touchEndClientX - touchStartClientX; 118 | var absDx = Math.abs(dx); 119 | 120 | var dy = touchEndClientY - touchStartClientY; 121 | var absDy = Math.abs(dy); 122 | 123 | if (Math.max(absDx, absDy) > 10) { 124 | // (right : left) : (down : up) 125 | self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); 126 | } 127 | }); 128 | }; 129 | 130 | KeyboardInputManager.prototype.restart = function (event) { 131 | event.preventDefault(); 132 | this.emit("restart"); 133 | }; 134 | 135 | KeyboardInputManager.prototype.keepPlaying = function (event) { 136 | event.preventDefault(); 137 | this.emit("keepPlaying"); 138 | }; 139 | 140 | KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) { 141 | var button = document.querySelector(selector); 142 | button.addEventListener("click", fn.bind(this)); 143 | button.addEventListener(this.eventTouchend, fn.bind(this)); 144 | }; 145 | -------------------------------------------------------------------------------- /site/lib/2048/local_storage_manager.js: -------------------------------------------------------------------------------- 1 | window.fakeStorage = { 2 | _data: {}, 3 | 4 | setItem: function (id, val) { 5 | return this._data[id] = String(val); 6 | }, 7 | 8 | getItem: function (id) { 9 | return this._data.hasOwnProperty(id) ? this._data[id] : undefined; 10 | }, 11 | 12 | removeItem: function (id) { 13 | return delete this._data[id]; 14 | }, 15 | 16 | clear: function () { 17 | return this._data = {}; 18 | } 19 | }; 20 | 21 | function LocalStorageManager() { 22 | this.bestScoreKey = "bestScore"; 23 | this.gameStateKey = "gameState"; 24 | 25 | var supported = this.localStorageSupported(); 26 | this.storage = supported ? window.localStorage : window.fakeStorage; 27 | } 28 | 29 | LocalStorageManager.prototype.localStorageSupported = function () { 30 | var testKey = "test"; 31 | var storage = window.localStorage; 32 | 33 | try { 34 | storage.setItem(testKey, "1"); 35 | storage.removeItem(testKey); 36 | return true; 37 | } catch (error) { 38 | return false; 39 | } 40 | }; 41 | 42 | // Best score getters/setters 43 | LocalStorageManager.prototype.getBestScore = function () { 44 | return this.storage.getItem(this.bestScoreKey) || 0; 45 | }; 46 | 47 | LocalStorageManager.prototype.setBestScore = function (score) { 48 | this.storage.setItem(this.bestScoreKey, score); 49 | }; 50 | 51 | // Game state getters/setters and clearing 52 | LocalStorageManager.prototype.getGameState = function () { 53 | var stateJSON = this.storage.getItem(this.gameStateKey); 54 | return stateJSON ? JSON.parse(stateJSON) : null; 55 | }; 56 | 57 | LocalStorageManager.prototype.setGameState = function (gameState) { 58 | this.storage.setItem(this.gameStateKey, JSON.stringify(gameState)); 59 | }; 60 | 61 | LocalStorageManager.prototype.clearGameState = function () { 62 | this.storage.removeItem(this.gameStateKey); 63 | }; 64 | -------------------------------------------------------------------------------- /site/lib/2048/tile.js: -------------------------------------------------------------------------------- 1 | function Tile(position, value) { 2 | this.x = position.x; 3 | this.y = position.y; 4 | this.value = value || 2; 5 | 6 | this.previousPosition = null; 7 | this.mergedFrom = null; // Tracks tiles that merged together 8 | } 9 | 10 | Tile.prototype.savePosition = function () { 11 | this.previousPosition = { x: this.x, y: this.y }; 12 | }; 13 | 14 | Tile.prototype.updatePosition = function (position) { 15 | this.x = position.x; 16 | this.y = position.y; 17 | }; 18 | 19 | Tile.prototype.serialize = function () { 20 | return { 21 | position: { 22 | x: this.x, 23 | y: this.y 24 | }, 25 | value: this.value 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /site/lib/audio-interface.coffee: -------------------------------------------------------------------------------- 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 | 17 | getBrowserAudioContext = -> 18 | CONTEXT_INTERFACES = [ 19 | 'AudioContext' 20 | 'webkitAudioContext' 21 | 'mozAudioContext' 22 | 'oAudioContext' 23 | 'msAudioContext' 24 | ] 25 | for name in CONTEXT_INTERFACES 26 | if window[name]? then return new window[name] 27 | return null 28 | 29 | getContextWhenReady = (audio, callback) -> 30 | doPlay = -> 31 | callback(getBrowserAudioContext()) 32 | 33 | onCanPlay = -> 34 | # Timeout hack for chrome 35 | setTimeout(doPlay, 20) 36 | 37 | if(audio.readyState < 3) then audio.addEventListener('canplay', onCanPlay) else onCanPlay() 38 | 39 | 40 | (this ? exports).getContextWhenReady = getContextWhenReady -------------------------------------------------------------------------------- /site/lib/gravity.coffee: -------------------------------------------------------------------------------- 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 | 17 | (this ? exports).Gravity = {} 18 | 19 | G = 6.67 # 6.67e-11 20 | DT = 0.5 21 | SPHERE_VOL = 4/3*Math.PI 22 | 23 | interpolatePoints = (a, b, t) -> 24 | return seen.P( 25 | a.x*(1.0 - t) + b.x*t 26 | a.y*(1.0 - t) + b.y*t 27 | a.z*(1.0 - t) + b.z*t 28 | ) 29 | 30 | class Gravity.Body 31 | constructor : (options) -> 32 | _.extend(@, _.defaults(options, 33 | mass : 10 34 | radius : 10 35 | position : seen.P() 36 | velocity : seen.P() 37 | force : seen.P() 38 | acceleration : seen.P() 39 | accelRk4 : [seen.P(), seen.P(), seen.P()] 40 | color : '#FFFFFF' 41 | )) 42 | 43 | integrate : -> 44 | @position.add @velocity.copy().multiply(DT).add @acceleration.copy().multiply(DT*DT*0.5) 45 | @velocity.add @acceleration.copy().multiply(DT) 46 | 47 | # Creates a new body that is the result of a collision between this and other. 48 | # Assuming positions and velocities will combine proportionally to mass. 49 | # Conserves volume and mass. 50 | collide: (other) -> 51 | t = other.mass / (@mass + other.mass) 52 | 53 | volA = SPHERE_VOL*Math.pow(@radius,3.0) 54 | volB = SPHERE_VOL*Math.pow(other.radius,3.0) 55 | newRadius = Math.pow((volA + volB) / SPHERE_VOL,1/3.0) 56 | 57 | return new Gravity.Body( 58 | name : @name + '+' + other.name 59 | mass : @mass + other.mass 60 | radius : newRadius 61 | position : interpolatePoints(@position, other.position, t) 62 | velocity : interpolatePoints(@velocity, other.velocity, t) 63 | force : @force.copy().add other.force 64 | acceleration : @acceleration.copy().add other.acceleration 65 | ) 66 | 67 | Gravity.Bodies = { 68 | random : (n) -> 69 | return [0...n].map (i) -> 70 | mass = Math.random()*30 71 | massN = n*5 72 | position = seen.P(Math.random()*800 - 400, Math.random()*800 - 400, Math.random()*800 - 400) 73 | speed = Math.sqrt(G * (mass + massN) / Math.sqrt(position.dot(position))) 74 | velocity = seen.P(Math.random()*10 - 5, Math.random()*10 - 5, Math.random()*10 - 5) 75 | velocity.subtract(position.copy().multiply(position.dot(velocity) / position.dot(position))) 76 | velocity.multiply(speed / Math.sqrt(velocity.dot(velocity))) 77 | new Gravity.Body 78 | name : "Body-#{i}" 79 | mass : mass 80 | position : position 81 | velocity : velocity 82 | radius : Math.random()*20 83 | } 84 | 85 | class Gravity.Simulation 86 | constructor : (@model) -> 87 | 88 | simulate : -> 89 | @model.ticks += 1 90 | 91 | @resetForces() 92 | collisions = @accumulateForces() 93 | @resolveCollisions(collisions) 94 | @integrateMotion() 95 | return collisions 96 | 97 | resetForces : -> 98 | # Reset forces 99 | for object in @model.objects 100 | object.force = seen.P() 101 | 102 | accumulateForces : -> 103 | # Accumulate forces 104 | collisions = [] 105 | for a, ai in @model.objects 106 | for b, bi in @model.objects 107 | continue unless bi > ai 108 | 109 | ab = a.position.copy().subtract(b.position) 110 | force = G * a.mass * b.mass / ab.dot(ab) 111 | dist = Math.sqrt(ab.dot(ab)) 112 | forceScale = force/dist 113 | a.force.add ab.copy().multiply(-forceScale) 114 | b.force.add ab.copy().multiply(forceScale) 115 | 116 | if dist < (a.radius + b.radius) 117 | collisions.push {a, b, ai, bi} 118 | return collisions 119 | 120 | resolveCollisions : (collisions) -> 121 | # Resolve collisions 122 | newObjects = {} 123 | for collision in collisions 124 | newA = newObjects[collision.ai] 125 | newB = newObjects[collision.bi] 126 | a = newA?.object ? collision.a 127 | b = newB?.object ? collision.b 128 | 129 | continue if a is b 130 | 131 | # collide 132 | c = a.collide(b) 133 | 134 | # update model 135 | if (ai = @model.objects.indexOf(a)) >= 0 then @model.objects.splice(ai,1) 136 | if (bi = @model.objects.indexOf(b)) >= 0 then @model.objects.splice(bi,1) 137 | @model.objects.push c 138 | 139 | # update newObjects 140 | ids = _([collision.ai, collision.bi, newA?.ids, newA?.ids]).flatten().compact().value() 141 | for id in ids 142 | newObjects[id] = { 143 | ids : ids 144 | object : c 145 | } 146 | return 147 | 148 | integrateMotion : -> 149 | # Integrate motion 150 | for object in @model.objects 151 | object.acceleration = object.force.copy().multiply(1/object.mass) 152 | object.integrate() 153 | 154 | -------------------------------------------------------------------------------- /site/lib/perlin-noise.coffee: -------------------------------------------------------------------------------- 1 | # adapted from http://freespace.virgin.net/hugo.elias/models/m_perlin.htm 2 | 3 | class Perlin2D 4 | noise : (x, y) -> 5 | n = x + y * 57 6 | n = (n << 13) ^ n 7 | result = ( 1.0 - ( (n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0) 8 | 9 | return result 10 | 11 | smooth : (x, y) -> 12 | corners = @noise(x - 1, y - 1) + @noise(x + 1, y - 1) + @noise(x - 1, y + 1) + @noise(x + 1, y + 1) 13 | sides = @noise(x - 1, y) + @noise(x + 1, y) + @noise(x, y - 1) + @noise(x, y + 1) 14 | center = @noise(x, y) 15 | return corners / 16 + sides / 8 + center / 4 16 | 17 | interpolate : (a, b, t) -> 18 | f = (1 - Math.cos(Math.PI * t)) * 0.5 19 | return a * (1 - f) + b * f 20 | 21 | interpolate2D : (x, y) -> 22 | ix = Math.floor(x) 23 | iy = Math.floor(y) 24 | fx = x - ix 25 | fy = y - iy 26 | 27 | v0 = @smooth(ix, iy) 28 | v1 = @smooth(ix + 1, iy) 29 | v2 = @smooth(ix, iy + 1) 30 | v3 = @smooth(ix + 1, iy + 1) 31 | 32 | x0 = @interpolate(v0, v1, fx) 33 | x1 = @interpolate(v2, v3, fx) 34 | return @interpolate(x0, x1, fy) 35 | 36 | perlin : (x, y) -> 37 | persistence = 0.2 38 | octaves = 4 39 | 40 | accum = 0 41 | for i in [0..octaves] 42 | frequency = 1 << i 43 | amplitude = Math.pow(persistence, i) 44 | accum += @interpolate2D(x * frequency, y * frequency) * amplitude 45 | return accum 46 | 47 | 48 | (this ? exports).Perlin2D = Perlin2D 49 | -------------------------------------------------------------------------------- /site/markdown/release-notes.md: -------------------------------------------------------------------------------- 1 | ### v0.2.5 2 | 3 | *Oct 8, 2015* 4 | 5 | + Add "pipe" shape. 6 | 7 | + Add Biovision BVH mocap file parser and mocap animator. 8 | 9 | + Add mocap skeleton demo. 10 | 11 | ### v0.2.4 12 | 13 | *Sep 23, 2015* 14 | 15 | + Integrate Simplex3D noise generator into library. 16 | 17 | ### v0.2.3 18 | 19 | *Mar 17, 2015* 20 | 21 | + Add depth of field demo that uses stackblur to create a depth-of-field effect. 22 | 23 | + Text shapes now compute an affine tranformation to utilize canvas and svg transforms on text. 24 | 25 | + Parsing hex and CSS color strings for easy material setting. 26 | 27 | ### v0.2.2 28 | 29 | *April 29, 2014* 30 | 31 | + Add bounding box calculation to render model. 32 | 33 | + Expose painters on seen namespace so they may be extended. 34 | 35 | + Expose more mouse events on the dragger interaction. 36 | 37 | ### v0.2.1 38 | 39 | *April 27, 2014* 40 | 41 | + Add 'g' tag to context utility method to easily use SVG groups. 42 | 43 | ### v0.2.0 44 | 45 | *April 11, 2014* 46 | 47 | + Fix specular phong shading calculation. 48 | 49 | + Move viewport into scene to make scene construction cleaner. 50 | 51 | + Cameras are now seen.Transformable objects 52 | 53 | + Points and shaders utilities use functions instead of raw objects (e.g. seen.Points.X()) for a more consistent API. 54 | 55 | + Animators have timestamps and time-delta and transitions have time-since-start and duration-fraction all in milliseconds for better animation consistency across environments. 56 | 57 | + Added transformation baking so transformables can be reset to a non-identity matrix. 58 | 59 | ### v0.1.5 60 | 61 | *April 10, 2014* 62 | 63 | + Change animator method to 'frame' instead of 'render' for a more agnostic API. 64 | 65 | + Add RenderAnimator. 66 | 67 | + Add Transitions and TransitionAnimator. 68 | 69 | + Adding touch interactions for swipe-to-rotate on mobile. 70 | 71 | ### v0.1.4 72 | 73 | *April 6, 2014* 74 | 75 | + Issue #2. Switch to using requestAnimationFrame instead of setTimeout for animation if available. 76 | 77 | ### v0.1.3 78 | 79 | *April 6, 2014* 80 | 81 | + Fixed bug where matrix scaling was not properly computed and viewport prescale and postscale were incorrect. 82 | 83 | + Added pyramid shape and updated extrude function to offset in any direction 84 | 85 | ### v0.1.2 86 | 87 | *April 2, 2014* 88 | 89 | + Add flag to disable renderModel caching (not recommended) 90 | 91 | + Prevent default scroll action on canvases when zoomer is attached. 92 | 93 | + More documentation for Docco. 94 | 95 | ### v0.1.1 96 | 97 | *March 25, 2014* 98 | 99 | + Removed default fill layer and change default create context method for simplicity and clarity. 100 | 101 | + Removed width/height requirement for creating contexts. Now width/height is only necessary to align viewport. 102 | 103 | + Updated tests to include both SVG and Canvas renderings 104 | 105 | ### v0.1.0 - *INITIAL RELEASE* 106 | 107 | *March 24, 2014* 108 | 109 | #### Features 110 | 111 | + Render 3D scenes into SVG or HTML5 Canvas elements. 112 | 113 | + Shape primivites : 114 | + tetrahedron 115 | + cube 116 | + sphere (sub-divided icosahedron) 117 | + patch (triangulated) 118 | + Wavefront .obj format parser 119 | 120 | + Perspective and orthographic projections. 1:1 pixel-aligned viewport in z=1 plane. 121 | 122 | + Hierarchically transformed and lit scene models. 123 | 124 | + Ambient, point, and directional light sources. 125 | 126 | + Phong ambient, diffuse and specular lighting model per surface. 127 | 128 | + Simple event-based animator. 129 | 130 | + Mouse drag and scroll adapters for mouse-look and mousewheel-zoom. 131 | 132 | + Scene layering and re-contextualization. Use multiple scenes in one context or use one scene in multiple contexts. 133 | 134 | + Customizeable shapes, painters, and shaders. 135 | 136 | + Performance: z-order painter's algorithm, backface culling, point rounding, rendermodel caching, lazy evaluation of tranformations. 137 | 138 | + No dependencies 139 | -------------------------------------------------------------------------------- /site/markdowns.coffee: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | title : 'Developer\'s Guide' 3 | route : 'guide' 4 | path : 'markdown/guide.md' 5 | , 6 | title : 'Release Notes' 7 | route : 'release-notes' 8 | path : 'markdown/release-notes.md' 9 | ] -------------------------------------------------------------------------------- /site/options.coffee: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cdns : { 3 | lodash : 4 | script : 'http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.legacy.js' 5 | jquery : 6 | script : 'http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js' 7 | jqueryui : 8 | style : 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css' 9 | script : 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js' 10 | highlightjs : 11 | style : 'http://yandex.st/highlightjs/8.0/styles/monokai_sublime.min.css' 12 | script : 'http://yandex.st/highlightjs/8.0/highlight.min.js' 13 | } 14 | styles : [ 15 | 'http://fonts.googleapis.com/css?family=Roboto' 16 | 'css/theme.css' 17 | ] 18 | scripts : [ 19 | 'lib/seen.min.js' 20 | 'http://cdnjs.cloudflare.com/ajax/libs/coffee-script/1.7.1/coffee-script.min.js' 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /site/views/demo-depth-of-field.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block caption %} 4 |

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 |
30 |

Shape

31 |
32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 |

Material

40 |
Hue
41 |
42 | 43 |
Shinyness
44 |
45 | 46 |
Specular Type
47 |
48 | 49 | 50 |
51 | 52 |

Lights

53 |
54 | 55 | 56 | 57 |
58 | 59 |

Lighting model

60 |
61 | 62 | 63 | 64 |
65 |
66 | 67 | 164 | {% endblock %} 165 | -------------------------------------------------------------------------------- /site/views/demo-mocap.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 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /site/views/demo-multi-views.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block canvases %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | {% endblock %} 16 | 17 | {% block caption %} 18 |

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 | 5 | 6 | 7 | 8 | 9 |

HTML5 Canvas

SVG

10 | {% endblock %} 11 | 12 | {% block demo %} 13 | 42 | {% endblock %} 43 | 44 | -------------------------------------------------------------------------------- /site/views/demo-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{title}} | seen.js 4 | {% for style in styles %} 5 | 6 | {% endfor %} 7 | {% for script in scripts %} 8 | 9 | {% endfor %} 10 | 11 | 12 |
13 |

{{title}}

14 |

{{description|raw}}

15 | 16 | {% include "snippets/demo-nav.html" %} 17 | 18 | {% block canvases %} 19 | 20 | {% endblock %} 21 | 22 |
23 | {% block caption %} 24 | {% endblock %} 25 |
26 | 27 | {% block demo %} 28 | {% endblock %} 29 | 30 | {% block code %} 31 | {% include "snippets/code-block.html" %} 32 | {% endblock %} 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /site/views/demo-text.html: -------------------------------------------------------------------------------- 1 | {% extends 'demo-template.html' %} 2 | 3 | {% block caption %} 4 |

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 | 5 | 6 | 7 | 8 | 9 |
10 | {% endblock %} 11 | 12 | {% block caption %} 13 |

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 | seen.js 4 | {% for style in styles %} 5 | 6 | {% endfor %} 7 | {% for script in scripts %} 8 | 9 | {% endfor %} 10 | 11 | 12 |
13 |

seen.jsRender 3D scenes into SVG or HTML5 Canvas.

14 | 15 | 16 | 17 |
18 | 19 |

Download

20 | 21 | 22 |
23 |
Download seen.min.js v{{version}}
24 |
25 |
26 | 27 |

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 |

Demos

40 | 41 | 46 | 47 |

Source

48 | The full source is available on github. 49 | 50 |

Guide

51 | To better understand the opinions of this library, please read the guide. 52 | 53 |

Docs

54 | Read the Docco-generated annotated source. 55 |
56 | 57 | 58 | 59 |
60 | 61 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /site/views/markdown-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{title}} | seen.js 4 | {% for style in styles %} 5 | 6 | {% endfor %} 7 | {% for script in scripts %} 8 | 9 | {% endfor %} 10 | 11 | 12 |
13 |

{{title}}

14 |
15 | {{markdown|raw}} 16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /site/views/snippets/code-block.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
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 | Dev Template | seen.js 4 | {% for script in scripts %} 5 | 6 | {% endfor %} 7 | 8 | 9 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -R spec 2 | --compilers coffee:coffee-script/register 3 | --timeout 30000 4 | --slow 200 -------------------------------------------------------------------------------- /test/mocha/color-test.coffee: -------------------------------------------------------------------------------- 1 | seen = require '../../dist/latest/seen' 2 | {assert} = require 'chai' 3 | 4 | assertColorsMatch = (c0, c1) -> 5 | assert.ok(c0) 6 | assert.ok(c1) 7 | assert.equal(c0.r, c1.r) 8 | assert.equal(c0.g, c1.g) 9 | assert.equal(c0.b, c1.b) 10 | assert.equal(c0.a, c1.a) 11 | 12 | describe 'color tests', -> 13 | it 'can create colors', -> 14 | c = seen.C(0,0,0) 15 | assert.ok(c) 16 | 17 | it 'can parse hex strings', -> 18 | c0 = seen.Colors.hex('#FF00FF') 19 | c1 = seen.Colors.parse('#FF00FF') 20 | assertColorsMatch(c0, c1) 21 | 22 | it 'can parse rgb strings', -> 23 | c0 = seen.Colors.rgb(0xFF, 0, 0xFF) 24 | c1 = seen.Colors.parse('rgb(255,0,255)') 25 | assertColorsMatch(c0, c1) 26 | 27 | it 'can parse rgba strings', -> 28 | c0 = seen.Colors.rgb(0xFF, 0, 0xFF, 0x80) 29 | c1 = seen.Colors.parse('rgba(255,0,255,0.5)') 30 | assertColorsMatch(c0, c1) 31 | 32 | it 'can default to black if it can\'t parse', -> 33 | c0 = seen.Colors.black() 34 | c1 = seen.Colors.parse('jabba the babba') 35 | assertColorsMatch(c0, c1) 36 | -------------------------------------------------------------------------------- /test/mocha/math-test.coffee: -------------------------------------------------------------------------------- 1 | seen = require '../../dist/latest/seen' 2 | {assert} = require 'chai' 3 | 4 | EPS = 1e-6 5 | 6 | describe 'math tests', -> 7 | it 'can translate a point', -> 8 | matrix = seen.M() 9 | matrix.translate(0,10,0) 10 | 11 | point = seen.P() 12 | point.transform(matrix) 13 | 14 | assert.closeTo(point.x, 0, EPS) 15 | assert.closeTo(point.y, 10, EPS) 16 | assert.closeTo(point.z, 0, EPS) 17 | 18 | matrix = seen.M() 19 | matrix.translate(0,10,0) 20 | matrix.translate(0,-10,0) 21 | matrix.translate(5,0,0) 22 | matrix.translate(0,0,-1) 23 | 24 | point = seen.P() 25 | point.transform(matrix) 26 | 27 | assert.closeTo(point.x, 5, EPS) 28 | assert.closeTo(point.y, 0, EPS) 29 | assert.closeTo(point.z, -1, EPS) 30 | 31 | it 'can rotate a point', -> 32 | matrix = seen.M() 33 | matrix.roty(Math.PI) 34 | 35 | point = seen.P(1,0,0) 36 | point.transform(matrix) 37 | 38 | assert.closeTo(point.x, -1, EPS) 39 | assert.closeTo(point.y, 0, EPS) 40 | assert.closeTo(point.z, 0, EPS) 41 | 42 | matrix = seen.M() 43 | matrix.roty(Math.PI) 44 | point.transform(matrix) 45 | 46 | assert.closeTo(point.x, 1, EPS) 47 | assert.closeTo(point.y, 0, EPS) 48 | assert.closeTo(point.z, 0, EPS) 49 | 50 | matrix = seen.M() 51 | matrix.rotz(Math.PI/2) 52 | point.transform(matrix) 53 | 54 | assert.closeTo(point.x, 0, EPS) 55 | assert.closeTo(point.y, 1, EPS) 56 | assert.closeTo(point.z, 0, EPS) 57 | 58 | it 'can rotate a point around another point', -> 59 | matrix = seen.M() 60 | matrix.translate(0,-1,0) 61 | matrix.roty(Math.PI) 62 | matrix.translate(0, 1,0) 63 | 64 | point = seen.P(1,1,0) 65 | point.transform(matrix) 66 | 67 | assert.closeTo(point.x, -1, EPS) 68 | assert.closeTo(point.y, 1, EPS) 69 | assert.closeTo(point.z, 0, EPS) 70 | 71 | it 'can scale a point', -> 72 | matrix = seen.M() 73 | matrix.scale(1,10,0) 74 | 75 | point = seen.P(1,1,1) 76 | point.transform(matrix) 77 | 78 | assert.closeTo(point.x, 1, EPS) 79 | assert.closeTo(point.y, 10, EPS) 80 | assert.closeTo(point.z, 0, EPS) 81 | 82 | it 'can scale a point around another point', -> 83 | matrix = seen.M() 84 | matrix.translate(0,-3,0) 85 | matrix.scale(10,10,10) 86 | 87 | point = seen.P(1,1,1) 88 | point.transform(matrix) 89 | 90 | assert.closeTo(point.x, 10, EPS) 91 | assert.closeTo(point.y, -20, EPS) 92 | assert.closeTo(point.z, 10, EPS) 93 | 94 | point = seen.P(2,3,4) 95 | point.transform(matrix) 96 | 97 | assert.closeTo(point.x, 20, EPS) 98 | assert.closeTo(point.y, 0, EPS) 99 | assert.closeTo(point.z, 40, EPS) 100 | -------------------------------------------------------------------------------- /test/mocha/model-test.coffee: -------------------------------------------------------------------------------- 1 | seen = require '../../dist/latest/seen' 2 | {assert} = require 'chai' 3 | 4 | describe 'model tests', -> 5 | it 'can create a model', -> 6 | model = new seen.Model() 7 | 8 | it 'can create a default model', -> 9 | model = seen.Models.default() 10 | assert.equal(3, model.lights.length) 11 | 12 | it 'can add and remove a sub model', -> 13 | model = new seen.Model() 14 | assert.equal(0, model.children.length) 15 | 16 | child = model.append() 17 | assert.equal(1, model.children.length) 18 | assert.equal(0, child.children.length) 19 | 20 | model.remove(child) 21 | assert.equal(0, model.children.length) 22 | 23 | it 'can add and remove a shape', -> 24 | model = new seen.Model() 25 | assert.equal(0, model.children.length) 26 | 27 | shape = seen.Shapes.cube() 28 | model.add(shape) 29 | assert.equal(1, model.children.length) 30 | model.remove(shape) 31 | assert.equal(0, model.children.length) 32 | 33 | it 'can add and remove a light', -> 34 | model = new seen.Model() 35 | assert.equal(0, model.lights.length) 36 | 37 | light = seen.Lights.point() 38 | model.add(light) 39 | assert.equal(1, model.lights.length) 40 | model.remove(light) 41 | assert.equal(0, model.lights.length) 42 | -------------------------------------------------------------------------------- /test/mocha/render-test.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | childProcess = require 'child_process' 3 | fs = require 'fs' 4 | phantomjs = require 'phantomjs' 5 | pngjs = require 'pngjs' 6 | {assert} = require 'chai' 7 | Q = require 'q' 8 | 9 | TAG_TYPES = [ 10 | 'svg' 11 | 'canvas' 12 | ] 13 | 14 | RENDERS = [ 15 | 'lights-ambient' 16 | 'lights-directional' 17 | 'lights-point' 18 | 'materials-metallic' 19 | 'materials-opacity' 20 | 'materials-specular' 21 | 'materials-stroke' 22 | 'shaders-ambient' 23 | 'shaders-diffuse' 24 | 'shaders-flat' 25 | 'shaders-phong' 26 | 'shapes-cube' 27 | 'shapes-icosahedron-0' 28 | 'shapes-icosahedron-1' 29 | 'shapes-icosahedron-2' 30 | 'shapes-icosahedron-3' 31 | 'shapes-icosahedron-4' 32 | #'shapes-text-0' 33 | 'shapes-patch' 34 | 'shapes-tetrahedon' 35 | ] 36 | 37 | compareRenders = (pathCanonical, pathTest, done) -> 38 | promises = [pathCanonical, pathTest].map (pathPng) -> 39 | defer = Q.defer() 40 | fs.createReadStream(pathPng) 41 | .pipe(new pngjs.PNG({filterType : 4})) 42 | .on('parsed', -> defer.resolve(@)) 43 | return defer.promise 44 | 45 | return Q.all(promises) 46 | .spread((pngCanonical, pngTest) -> 47 | # Sadly, we can't just compare the buffers with deepEquals because mocha stalls 48 | assert.equal(pngCanonical.data.length, pngTest.data.length) 49 | for i in [0...pngCanonical.data.length] 50 | assert.equal(pngCanonical.data[i], pngTest.data[i]) 51 | return 52 | ) 53 | .done(done) 54 | 55 | describe 'render smoke test', -> 56 | it 'renders test scenes without errors', (done) -> 57 | testPath = path.join(__dirname, '..', 'phantom', 'render-scenes.coffee') 58 | childProcess.execFile(phantomjs.path, [testPath], (err, stdout, stderr) -> 59 | assert.propertyVal(stdout, 'length', 0) 60 | assert.propertyVal(stderr, 'length', 0) 61 | done(err) 62 | ) 63 | 64 | describe 'rendered all test scenes', () -> 65 | TAG_TYPES.forEach (tagType) -> 66 | RENDERS.forEach (render) -> 67 | it "rendered #{tagType} #{render}", -> 68 | assert(fs.existsSync(path.join(__dirname, '..', 'phantom', 'renders', "#{tagType}-#{render}.png"))) 69 | 70 | describe 'all renders match canonical renders', () -> 71 | TAG_TYPES.forEach (tagType) -> 72 | RENDERS.forEach (render) -> 73 | it "rendered #{tagType} #{render} matches canonical", (done) -> 74 | compareRenders( 75 | path.join(__dirname, '..', 'phantom', 'canonical', "#{tagType}-#{render}.png") 76 | path.join(__dirname, '..', 'phantom', 'renders', "#{tagType}-#{render}.png") 77 | done 78 | ) 79 | -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-lights-ambient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-lights-ambient.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-lights-directional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-lights-directional.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-lights-point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-lights-point.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-materials-metallic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-materials-metallic.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-materials-opacity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-materials-opacity.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-materials-specular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-materials-specular.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-materials-stroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-materials-stroke.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shaders-ambient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shaders-ambient.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shaders-diffuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shaders-diffuse.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shaders-flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shaders-flat.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shaders-phong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shaders-phong.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shapes-cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shapes-cube.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shapes-icosahedron-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shapes-icosahedron-0.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shapes-icosahedron-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shapes-icosahedron-1.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shapes-icosahedron-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shapes-icosahedron-2.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shapes-icosahedron-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shapes-icosahedron-3.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shapes-icosahedron-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shapes-icosahedron-4.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shapes-patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shapes-patch.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shapes-tetrahedon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shapes-tetrahedon.png -------------------------------------------------------------------------------- /test/phantom/canonical/canvas-shapes-text-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/canvas-shapes-text-0.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-lights-ambient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-lights-ambient.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-lights-directional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-lights-directional.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-lights-point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-lights-point.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-materials-metallic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-materials-metallic.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-materials-opacity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-materials-opacity.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-materials-specular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-materials-specular.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-materials-stroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-materials-stroke.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shaders-ambient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shaders-ambient.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shaders-diffuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shaders-diffuse.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shaders-flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shaders-flat.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shaders-phong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shaders-phong.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shapes-cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shapes-cube.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shapes-icosahedron-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shapes-icosahedron-0.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shapes-icosahedron-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shapes-icosahedron-1.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shapes-icosahedron-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shapes-icosahedron-2.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shapes-icosahedron-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shapes-icosahedron-3.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shapes-icosahedron-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shapes-icosahedron-4.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shapes-patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shapes-patch.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shapes-tetrahedon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shapes-tetrahedon.png -------------------------------------------------------------------------------- /test/phantom/canonical/svg-shapes-text-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themadcreator/seen/d8946b3b97b9814e78f79334b9fd6349b9022289/test/phantom/canonical/svg-shapes-text-0.png -------------------------------------------------------------------------------- /test/phantom/phantom-scenes.coffee: -------------------------------------------------------------------------------- 1 | scenes = [ 2 | 3 | # =============================================== 4 | # 5 | # SHAPES 6 | # 7 | # =============================================== 8 | 9 | png : 'shapes-tetrahedon.png' 10 | script : -> 11 | window.test.model.add seen.Shapes.tetrahedron() 12 | .scale(window.test.height*0.2) 13 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 14 | , 15 | png : 'shapes-cube.png' 16 | script : -> 17 | window.test.model.add seen.Shapes.cube() 18 | .scale(window.test.height*0.2) 19 | .rotz(Math.PI/4) 20 | .roty(Math.PI/4) 21 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 22 | , 23 | png : 'shapes-patch.png' 24 | script : -> 25 | window.test.model.add seen.Shapes.patch(5, 5) 26 | .scale(window.test.height*0.1) 27 | .translate(-window.test.width*0.2,-window.test.height*0.2,0) 28 | .rotx(-Math.PI/12) 29 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 30 | , 31 | png : 'shapes-icosahedron-0.png' 32 | script : -> 33 | window.test.model.add seen.Shapes.sphere(0) 34 | .scale(window.test.height*0.4) 35 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 36 | , 37 | png : 'shapes-icosahedron-1.png' 38 | script : -> 39 | window.test.model.add seen.Shapes.sphere(1) 40 | .scale(window.test.height*0.4) 41 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 42 | , 43 | png : 'shapes-icosahedron-2.png' 44 | script : -> 45 | window.test.model.add seen.Shapes.sphere(2) 46 | .scale(window.test.height*0.4) 47 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 48 | , 49 | png : 'shapes-icosahedron-3.png' 50 | script : -> 51 | window.test.model.add seen.Shapes.sphere(3) 52 | .scale(window.test.height*0.4) 53 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 54 | , 55 | png : 'shapes-icosahedron-4.png' 56 | script : -> 57 | window.test.model.add seen.Shapes.sphere(4) 58 | .scale(window.test.height*0.4) 59 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 60 | , 61 | png : 'shapes-text-0.png' 62 | script : -> 63 | window.test.model.add seen.Shapes.text('seen.js', {font : '20px sans-serif'}) 64 | .rotz(0.1) 65 | .rotx(0.1) 66 | .fill('#0088FF') 67 | , 68 | 69 | # =============================================== 70 | # 71 | # LIGHTS 72 | # 73 | # =============================================== 74 | 75 | png : 'lights-ambient.png' 76 | script : -> 77 | window.test.model.add seen.Shapes.sphere(2) 78 | .scale(window.test.height*0.4) 79 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 80 | window.test.model.lights = [ 81 | seen.Lights.ambient( 82 | color : seen.Colors.hex('#0088FF') 83 | intensity : 0.01 84 | ) 85 | ] 86 | , 87 | png : 'lights-directional.png' 88 | script : -> 89 | window.test.model.add seen.Shapes.sphere(2) 90 | .scale(window.test.height*0.4) 91 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 92 | window.test.model.lights = [ 93 | seen.Lights.directional( 94 | normal : seen.P(-1, 1, 1).normalize() 95 | color : seen.Colors.hex('#0088FF') 96 | intensity : 0.01 97 | ) 98 | ] 99 | , 100 | png : 'lights-point.png' 101 | script : -> 102 | window.test.model.add seen.Shapes.sphere(2) 103 | .scale(window.test.height*0.4) 104 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 105 | window.test.model.lights = [ 106 | seen.Lights.point( 107 | point : seen.P(-100, 100, 100) 108 | color : seen.Colors.hex('#0088FF') 109 | intensity : 0.01 110 | ) 111 | ] 112 | 113 | 114 | # =============================================== 115 | # 116 | # MATERIALS 117 | # 118 | # =============================================== 119 | 120 | , 121 | png : 'materials-stroke.png' 122 | script : -> 123 | window.test.model.add seen.Shapes.sphere(2) 124 | .scale(window.test.height*0.4) 125 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 126 | .stroke(new seen.Material seen.Colors.black(), {'stroke-width' : 1}) # TODO this currently doesnt work because its on the surface 127 | , 128 | png : 'materials-opacity.png' 129 | script : -> 130 | window.test.scene.cullBackfaces = false 131 | window.test.model.add seen.Shapes.sphere(2) 132 | .scale(window.test.height*0.4) 133 | .fill(new seen.Material seen.Colors.rgb(255, 136, 0, 80)) 134 | , 135 | png : 'materials-specular.png' 136 | script : -> 137 | window.test.model.add seen.Shapes.sphere(2) 138 | .scale(window.test.height*0.4) 139 | .fill(new seen.Material seen.Colors.hex('#FF8800'), {specularExponent : 20}) 140 | , 141 | png : 'materials-metallic.png' 142 | script : -> 143 | window.test.model.add seen.Shapes.sphere(2) 144 | .scale(window.test.height*0.4) 145 | .fill(new seen.Material seen.Colors.hex('#FF8800'), {metallic : true, specularColor : seen.Colors.hex('#0088FF')}) 146 | , 147 | 148 | # =============================================== 149 | # 150 | # SHADERS 151 | # 152 | # =============================================== 153 | 154 | png : 'shaders-phong.png' 155 | script : -> 156 | window.test.model.add seen.Shapes.sphere(2) 157 | .scale(window.test.height*0.4) 158 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 159 | window.test.scene.shader = seen.Shaders.phong() 160 | , 161 | png : 'shaders-diffuse.png' 162 | script : -> 163 | window.test.model.add seen.Shapes.sphere(2) 164 | .scale(window.test.height*0.4) 165 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 166 | window.test.scene.shader = seen.Shaders.diffuse() 167 | , 168 | png : 'shaders-ambient.png' 169 | script : -> 170 | window.test.model.add seen.Shapes.sphere(2) 171 | .scale(window.test.height*0.4) 172 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 173 | window.test.scene.shader = seen.Shaders.ambient() 174 | , 175 | png : 'shaders-flat.png' 176 | script : -> 177 | window.test.model.add seen.Shapes.sphere(2) 178 | .scale(window.test.height*0.4) 179 | .fill(new seen.Material seen.Colors.hex('#FF8800')) 180 | window.test.scene.shader = seen.Shaders.flat() 181 | 182 | 183 | # SVG vs. Canvas 184 | 185 | # Painters 186 | ] 187 | 188 | module.exports = scenes 189 | -------------------------------------------------------------------------------- /test/phantom/render-favicon.coffee: -------------------------------------------------------------------------------- 1 | 2 | initPage = (page) -> 3 | page.onConsoleMessage = (msg) -> console.log(msg) 4 | page.onError = (msg) -> console.error(msg) 5 | 6 | width = 260 7 | height = 260 8 | 9 | page.setContent """ 10 | 11 | 12 | 13 | """, 'test.html' 14 | 15 | page.viewportSize = { 16 | width : width 17 | height : height 18 | } 19 | 20 | page.clipRect = { 21 | top : 0 22 | left : 0 23 | width : width 24 | height : height 25 | } 26 | 27 | page.injectJs('dist/latest/seen.min.js') 28 | 29 | page = require('webpage').create() 30 | initPage(page) 31 | page.evaluate(-> 32 | width = 260 33 | height = 260 34 | shape = seen.Shapes.sphere(1).scale(110).roty(0.2).rotx(0.1) 35 | seen.Colors.randomSurfaces2(shape) 36 | model = seen.Models.default().add(shape) 37 | scene = new seen.Scene 38 | model : model 39 | viewport : seen.Viewports.center(width, height) 40 | seen.Context('seen-canvas', scene).render() 41 | ) 42 | 43 | # Further generation done with http://realfavicongenerator.net/ 44 | page.render('favicon-260x260.png') 45 | phantom.exit() 46 | -------------------------------------------------------------------------------- /test/phantom/render-scenes.coffee: -------------------------------------------------------------------------------- 1 | 2 | 3 | initPage = (tagType, page) -> 4 | page.onConsoleMessage = (msg) -> console.log(msg) 5 | page.onError = (msg) -> console.error(msg) 6 | 7 | width = 100 8 | height = 100 9 | 10 | page.setContent """ 11 | 12 | <#{tagType} id="seen-canvas" width="#{width}" height="#{height}"> 13 | 14 | """, 'test.html' 15 | 16 | page.viewportSize = { 17 | width : width 18 | height : height 19 | } 20 | 21 | page.clipRect = { 22 | top : 0 23 | left : 0 24 | width : width 25 | height : height 26 | } 27 | 28 | page.injectJs('dist/latest/seen.min.js') 29 | 30 | evaluateScene = (page, scene) -> 31 | page.evaluate(-> 32 | window.test = do -> 33 | @width = 100 34 | @height = 100 35 | @model = seen.Models.default() 36 | @scene = new seen.Scene 37 | model : @model 38 | viewport : seen.Viewports.center(@width, @height) 39 | @context = seen.Context('seen-canvas', @scene) 40 | return @ 41 | ) 42 | page.evaluate(scene.script) 43 | page.evaluate(-> 44 | window.test.context.render() 45 | ) 46 | 47 | scenes = require('./phantom-scenes') 48 | for tagType in ['svg', 'canvas'] 49 | for scene in scenes 50 | page = require('webpage').create() 51 | initPage(tagType, page) 52 | evaluateScene(page, scene) 53 | page.render("test/phantom/renders/#{tagType}-#{scene.png}") 54 | 55 | phantom.exit() 56 | -------------------------------------------------------------------------------- /test/phantom/renders/.gitignore: -------------------------------------------------------------------------------- 1 | *.png --------------------------------------------------------------------------------