├── README.md ├── example └── index.js ├── index.coffee ├── package.json └── spriter.coffee /README.md: -------------------------------------------------------------------------------- 1 | # Text 3D Particles 2 | 3 | Hard to explain, but looks cool. 4 | 5 | ![star](http://i.imgur.com/hsVV9I4.gif) 6 | 7 | To see it in action: 8 | 9 | npm run example 10 | 11 | Usage: 12 | 13 | ```js 14 | 15 | var text3dParticles = require('text-3d-particles') 16 | 17 | var opts = 18 | { width: 400 19 | , height: 400 20 | , text: '★' 21 | , foreground: '#707070' 22 | , background: '#f6f6f6' 23 | , duration: 6000 24 | } 25 | 26 | var textGraph = text3dParticles(opts, function() { 27 | console.log('Animation completed.') 28 | }) 29 | 30 | document.body.appendChild(textGraph.el) 31 | 32 | ``` 33 | 34 | # License 35 | 36 | MIT 37 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var text3dParticles = require('../') 2 | 3 | var opts = 4 | { width: 800 5 | , height: 400 6 | , text: '★' 7 | , fontSize: 400 8 | , density: 15 9 | , nodeSize: 20 10 | , foreground: '#707070' 11 | , background: '#f6f6f6' 12 | , duration: 10000 13 | , thetaStart: 0 14 | , thetaEnd: 2 * Math.PI 15 | , loop: true 16 | } 17 | 18 | var particles = text3dParticles(opts, function() { 19 | console.log('Animation completed.') // would get called if opts.loop != true 20 | }) 21 | 22 | document.body.appendChild(particles.el) 23 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | Spriter = require './spriter.coffee' 2 | 3 | module.exports = (opts = {}, cb = ->) -> 4 | opts.density ?= 20 5 | opts.background ?= '#f6f6f6' 6 | opts.width ?= 800 7 | opts.height ?= 400 8 | opts.text ?= 'D' 9 | opts.fontSize ?= opts.height 10 | opts.nodeSize ?= opts.height / 20 11 | opts.loop ?= false 12 | 13 | if opts.canvas 14 | canvas = opts.canvas 15 | opts.width = canvas.width 16 | opts.height = canvas.height 17 | else 18 | canvas = document.createElement 'canvas' 19 | canvas.width = w = opts.width 20 | canvas.height = h = opts.height 21 | 22 | context = canvas.getContext '2d' 23 | 24 | context.font = "bold #{opts.fontSize}px Georgia" 25 | context.textAlign = 'center' 26 | context.fillText opts.text, w/2, h/2 + opts.fontSize/4 27 | 28 | spriter = new Spriter canvas, opts, cb 29 | spriter.el.classList.add 'text-graph-3d' 30 | return spriter 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text-3d-particles", 3 | "version": "0.4.0", 4 | "description": "Hard to explain, but looks cool.", 5 | "main": "index.coffee", 6 | "scripts": { 7 | "example": "beefy index.js --live --cwd example" 8 | }, 9 | "author": "David Guttman", 10 | "license": "MIT", 11 | "dependencies": { 12 | "coffeeify": "^0.6.0", 13 | "three": "^0.66.2", 14 | "ease-component": "^1.0.0", 15 | "wildemitter": "^1.0.1" 16 | }, 17 | "devDependencies": { 18 | "beefy": "^1.1.0", 19 | "browserify": "^3.33.0" 20 | }, 21 | "browserify": { 22 | "transform": [ 23 | "coffeeify" 24 | ] 25 | }, 26 | "browser": "index.coffee", 27 | "directories": { 28 | "example": "example" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git://github.com/davidguttman/text-3d-particles.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/davidguttman/text-3d-particles/issues" 36 | }, 37 | "homepage": "https://github.com/davidguttman/text-3d-particles" 38 | } 39 | -------------------------------------------------------------------------------- /spriter.coffee: -------------------------------------------------------------------------------- 1 | THREE = require 'three' 2 | Emitter = require 'wildemitter' 3 | ease = require 'ease-component' 4 | 5 | cameraZ = 600 6 | 7 | originVector = new THREE.Vector3 0,0,0 8 | 9 | module.exports = Spriter = (@shapeCanvas, @opts = {}, @cb) -> 10 | Emitter.call this 11 | {density, background} = @opts 12 | 13 | ar = @shapeCanvas.width / @shapeCanvas.height 14 | 15 | @el = document.createElement 'div' 16 | 17 | @camera = new THREE.PerspectiveCamera 75, ar, 1, 5000 18 | @camera.position.z = cameraZ 19 | 20 | @scene = new THREE.Scene 21 | 22 | @material = new THREE.SpriteMaterial 23 | map: new THREE.Texture @generateSprite() 24 | blending: THREE.MultiplyBlending 25 | 26 | @addParticles @shapeCanvas, density 27 | 28 | @renderer = new THREE.CanvasRenderer 29 | @renderer.setClearColor @opts.background 30 | @renderer.setSize @shapeCanvas.width, @shapeCanvas.height 31 | 32 | @el.appendChild @renderer.domElement 33 | 34 | @initAnimation() 35 | unless @opts.autoStart is false 36 | @playing = true 37 | @animate() 38 | 39 | return this 40 | 41 | Spriter.prototype = new Emitter 42 | 43 | Spriter::addParticles = (shapeCanvas, density = 20) -> 44 | shapeContext = @shapeCanvas.getContext '2d' 45 | img = shapeContext.getImageData 0, 0, @shapeCanvas.width, @shapeCanvas.height 46 | 47 | for x in [0...@shapeCanvas.width] by density 48 | for y in [0...@shapeCanvas.height] by density 49 | color = getColor img, x, y 50 | if color[3] > 0 51 | xo = x * 2 - @shapeCanvas.width 52 | yo = @shapeCanvas.height - (y * 2) 53 | particle = @initParticle xo, yo 54 | @scene.add particle 55 | 56 | Spriter::initParticle = (x, y) -> 57 | particle = new THREE.Sprite @material 58 | 59 | cz = @camera.position.z 60 | z = gaussRand 0, cz/6 61 | 62 | x1 = x - (x*z/cz) 63 | y1 = y - (y*z/cz) 64 | z1 = z 65 | particle.position.set x1, y1, z1 66 | 67 | particle.scale.x = Math.random() * @opts.nodeSize + @opts.nodeSize/2 68 | particle.scale.y = particle.scale.x 69 | 70 | return particle 71 | 72 | Spriter::initAnimation = -> 73 | @thetaStart = @opts.thetaStart ? 1.10 * Math.PI 74 | @thetaEnd = @opts.thetaEnd ? 2 * Math.PI 75 | @theta = @thetaStart 76 | 77 | @duration = @opts.duration or 10000 78 | @timeStart = @timeEnd = null 79 | 80 | Spriter::animate = -> 81 | return unless @playing 82 | requestAnimationFrame @animate.bind(this) 83 | @render() 84 | 85 | Spriter::generateSprite = -> 86 | canvas = document.createElement 'canvas' 87 | canvas.width = w = @opts.nodeSize 88 | canvas.height = h = @opts.nodeSize 89 | 90 | ctx = canvas.getContext '2d' 91 | 92 | ctx.fillStyle = @opts.foreground ? 'rgb(46,46,46)' 93 | 94 | r = canvas.width/4 95 | ctx.beginPath() 96 | ctx.arc w/2, h/2, r, 0, 2 * Math.PI, false 97 | ctx.fill() 98 | ctx.closePath() 99 | 100 | return canvas 101 | 102 | Spriter::render = (p) -> 103 | now = Date.now() 104 | @timeStart = @timeStart or now 105 | @timeEnd = @timeEnd or @timeStart + @duration 106 | 107 | if @theta >= @thetaEnd * 0.9999 108 | if @opts.loop 109 | return @initAnimation() 110 | else 111 | @theta = @thetaEnd 112 | @playing = false 113 | @cb() 114 | 115 | p = p ? (now - @timeStart) / @duration 116 | val = ease.inOutSine p 117 | @theta = @thetaStart + (@thetaEnd - @thetaStart) * val 118 | 119 | @camera.position.x = Math.sin(@theta) * cameraZ 120 | @camera.position.y = Math.sin(@theta) * cameraZ 121 | @camera.position.z = Math.cos(@theta) * cameraZ 122 | @camera.lookAt originVector 123 | @renderer.render @scene, @camera 124 | @emit 'progress', p 125 | 126 | getColor = (img, x, y) -> 127 | data = img.data 128 | i = (y * img.width + x) * 4 129 | [data[i], data[i+1], data[i+2], data[i+3]] 130 | 131 | gaussRand = (mean=0.5, stdev=1/9) -> 132 | r = (Math.random() * 2 - 1) + (Math.random() * 2 - 1) + (Math.random() * 2 - 1) 133 | gauss = r * stdev + mean 134 | --------------------------------------------------------------------------------