├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── images └── example.gif ├── package.json └── src └── Graphics ├── Vis.js ├── Vis.purs └── Vis └── Example.purs /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /yarn.lock 3 | .psci_modules/ 4 | bower_components/ 5 | js/ 6 | output/ 7 | .psc-ide-port 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | sudo: required 4 | node_js: stable 5 | install: 6 | - npm install -g bower 7 | - npm install 8 | - bower install 9 | script: 10 | - npm run -s build 11 | after_success: 12 | - >- 13 | test $TRAVIS_TAG && 14 | echo $GITHUB_TOKEN | pulp login && 15 | echo y | pulp publish --no-push 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Phil Freeman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-graphics-vis 2 | 3 | A library for interactively creating graphics visualizations in the browser using PSCi and the WebAudio and Canvas APIs. 4 | 5 | Note: Internet Explorer and Safari are [not currently supported](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Browser_compatibility). 6 | 7 | ## Instructions 8 | 9 | - Install the latest (>= 0.10.2) PSCi. 10 | - Start PSCi with the `--port` option: `pulp psci -- --port 8080` 11 | - Open `http://localhost:8080/` 12 | - Evaluate expressions in PSCi: 13 | 14 | ```text 15 | PSCi, version 0.10.2 16 | Type :? for help 17 | 18 | Bundling Javascript... 19 | Serving http://localhost:8080/. Waiting for connections... 20 | 21 | > import Graphics.Vis.Example 22 | > animate scene 23 | ``` 24 | 25 | - Play some music and enjoy the show! 26 | 27 | ![Example](images/example.gif) 28 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-graphics-vis", 3 | "authors": [ 4 | "Phil Freeman " 5 | ], 6 | "license": "MIT", 7 | "ignore": [ 8 | "**/.*", 9 | "node_modules", 10 | "bower_components", 11 | "test", 12 | "tests" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/paf31/purescript-graphics-vis.git" 17 | }, 18 | "dependencies": { 19 | "purescript-drawing": "^4.0.0", 20 | "purescript-psci-support": "^4.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /images/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paf31/purescript-graphics-vis/7eefd14163a652890eba6f4cf942d2417d06cd04/images/example.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf output && rimraf .pulp-cache", 5 | "build": "pulp build" 6 | }, 7 | "devDependencies": { 8 | "pulp": "^11.0.2", 9 | "purescript": "^0.13.5", 10 | "purescript-psa": "^0.5.1", 11 | "rimraf": "^2.7.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Graphics/Vis.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This will be a persistent reference to the canvas 4 | // we will use for rendering. 5 | var canvas; 6 | 7 | // Create the audio context and an analyser with 8 | // 64 entries. 9 | var audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 10 | 11 | var analyser = audioCtx.createAnalyser(); 12 | analyser.fftSize = 64; 13 | 14 | // Create a buffer to hold the frequency data. 15 | var bufferLength = analyser.frequencyBinCount; 16 | var dataArray = new Uint8Array(bufferLength); 17 | 18 | // See https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia 19 | // 20 | // BEGIN SHIM 21 | // Older browsers might not implement mediaDevices at all, so we set an empty object first 22 | if (navigator.mediaDevices === undefined) { 23 | navigator.mediaDevices = {}; 24 | } 25 | 26 | // Some browsers partially implement mediaDevices. We can't just assign an object 27 | // with getUserMedia as it would overwrite existing properties. 28 | // Here, we will just add the getUserMedia property if it's missing. 29 | if (navigator.mediaDevices.getUserMedia === undefined) { 30 | navigator.mediaDevices.getUserMedia = function(constraints) { 31 | 32 | // First get ahold of the legacy getUserMedia, if present 33 | var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; 34 | 35 | // Some browsers just don't implement it - return a rejected promise with an error 36 | // to keep a consistent interface 37 | if (!getUserMedia) { 38 | return Promise.reject(new Error('getUserMedia is not implemented in this browser')); 39 | } 40 | 41 | // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise 42 | return new Promise(function(resolve, reject) { 43 | getUserMedia.call(navigator, constraints, resolve, reject); 44 | }); 45 | } 46 | } 47 | // END SHIM 48 | 49 | // Connect the analyser to the microphone input 50 | // (may require user approval) 51 | navigator.mediaDevices.getUserMedia({ audio: true }) 52 | .then(function(stream) { 53 | var source = audioCtx.createMediaStreamSource(stream); 54 | source.connect(analyser); 55 | }) 56 | .catch(function(error) { 57 | console.error(error); 58 | }); 59 | 60 | // Create the canvas if it has not already been created. 61 | exports.createCanvas = function() { 62 | if (!canvas) { 63 | canvas = document.createElement("canvas"); 64 | document.body.appendChild(canvas); 65 | canvas.width = window.innerWidth; 66 | canvas.height = window.innerHeight; 67 | } 68 | return canvas; 69 | }; 70 | 71 | // This is a persistent reference to the most recent 72 | // timer object used to render a scene. 73 | var lastInterval; 74 | 75 | // Animate a scene by replacing lastInterval with 76 | // a new timer which will render the new scene. 77 | exports.animateImpl = function(f) { 78 | return function() { 79 | lastInterval && window.clearInterval(lastInterval); 80 | lastInterval = window.setInterval(function() { 81 | // Read the data into the byte array 82 | analyser.getByteFrequencyData(dataArray); 83 | f(new Date().getTime())(dataArray.slice())(); 84 | }, 1000 / 60); 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /src/Graphics/Vis.purs: -------------------------------------------------------------------------------- 1 | module Graphics.Vis 2 | ( FFT 3 | , graphics 4 | , animate 5 | , bass 6 | , mids 7 | , treble 8 | , module Color 9 | , module Data.Monoid 10 | , module Graphics.Drawing 11 | , module Math 12 | , module Prelude 13 | ) where 14 | 15 | import Prelude 16 | import Graphics.Drawing as Graphics.Drawing 17 | import Color (Color, hsl) 18 | import Effect (Effect) 19 | import Data.Array (drop, length, take) 20 | import Data.Foldable (sum) 21 | import Data.Int (toNumber) 22 | import Data.Monoid (class Monoid, mempty) as Data.Monoid 23 | import Graphics.Canvas (CanvasElement, getContext2D, getCanvasWidth, getCanvasHeight, setFillStyle, save, restore, fillRect) 24 | import Graphics.Drawing (Drawing, Shape, render, circle, translate, outlined, outlineColor, lineWidth, filled) 25 | import Math (Radians, abs, acos, asin, atan, atan2, ceil, cos, e, exp, floor, ln10, ln2, log, log10e, log2e, pi, pow, remainder, round, sin, sqrt, sqrt1_2, sqrt2, tan, trunc, (%)) 26 | 27 | -- | A frequency spectrum from the WebAudio API. 28 | type FFT = Array Number 29 | 30 | foreign import animateImpl 31 | :: (Number -> FFT -> Effect Unit) 32 | -> Effect Unit 33 | 34 | foreign import createCanvas 35 | :: Effect CanvasElement 36 | 37 | -- | Render a drawing to the browser canvas, replacing any 38 | -- | existing drawing. 39 | graphics :: Drawing -> Effect Unit 40 | graphics scene' = do 41 | canvas <- createCanvas 42 | context <- getContext2D canvas 43 | width <- getCanvasWidth canvas 44 | height <- getCanvasHeight canvas 45 | _ <- save context 46 | _ <- setFillStyle context "rgba(255,255,255,0.1)" 47 | _ <- fillRect context { x: 0.0, y: 0.0, width: width, height: height } 48 | _ <- restore context 49 | render context scene' 50 | 51 | -- | Render an animation which depends on the current time 52 | -- | and audio input. 53 | animate :: (Number -> FFT -> Drawing) -> Effect Unit 54 | animate f = animateImpl (\t fft -> graphics (f t fft)) 55 | 56 | average :: FFT -> Number 57 | average xs = sum xs / toNumber (length xs) 58 | 59 | -- | Get a value which represents the bass level in the input audio. 60 | bass :: FFT -> Number 61 | bass fft = average (take 8 fft) / 48.0 62 | 63 | -- | Get a value which represents the mids level in the input audio. 64 | mids :: FFT -> Number 65 | mids fft = average (take 8 (drop 4 fft)) / 48.0 66 | 67 | -- | Get a value which represents the treble level in the input audio. 68 | treble :: FFT -> Number 69 | treble fft = average (take 8 (drop 8 fft)) / 12.0 70 | -------------------------------------------------------------------------------- /src/Graphics/Vis/Example.purs: -------------------------------------------------------------------------------- 1 | module Graphics.Vis.Example 2 | ( scene 3 | , module Graphics.Vis 4 | ) where 5 | 6 | import Prelude 7 | 8 | import Color (Color, hsl) 9 | import Color.Scheme.Harmonic (triad) 10 | import Data.Foldable (fold) 11 | import Data.Monoid (mempty) as Data.Monoid 12 | import Graphics.Vis (FFT, animate, graphics, bass, mids, treble) 13 | import Graphics.Drawing (Drawing, Shape, circle, filled, lineWidth, outlineColor, outlined, translate) 14 | import Graphics.Drawing as Graphics.Drawing 15 | import Math (cos, sin) 16 | 17 | -- | Test this in PSCi by importing this module and 18 | -- | evaluating `animate scene`. 19 | scene :: Number -> FFT -> Drawing 20 | scene t fft = 21 | case triad hue of 22 | [c1, c2, c3] -> 23 | fold 24 | [ translate 25 | (sin (t / 250.0) * 40.0 + 400.0) 26 | (cos (t / 200.0) * 40.0 + 400.0) 27 | (scale' (bass fft + 0.5) 28 | (outlined (outlineColor c1 <> lineWidth (bass fft)) shape)) 29 | , translate 30 | (sin (t / 350.0) * 40.0 + 400.0) 31 | (cos (t / 300.0) * 40.0 + 400.0) 32 | (scale' (mids fft + 0.5) 33 | (outlined (outlineColor c2 <> lineWidth (mids fft)) shape)) 34 | , translate 35 | (sin (t / 450.0) * 40.0 + 400.0) 36 | (cos (t / 400.0) * 40.0 + 400.0) 37 | (scale' (treble fft + 0.5) 38 | (outlined (outlineColor c3 <> lineWidth (treble fft)) shape)) 39 | , satellite 400.0 40 | , satellite 450.0 41 | , satellite 500.0 42 | ] 43 | _ -> Data.Monoid.mempty 44 | where 45 | shape :: Shape 46 | shape = circle 0.0 0.0 50.0 47 | 48 | scale' :: Number -> Drawing -> Drawing 49 | scale' s = Graphics.Drawing.scale s s 50 | 51 | hue :: Color 52 | hue = hsl (bass fft * 128.0) (0.5) (0.5) 53 | 54 | satellite :: Number -> Drawing 55 | satellite offset = 56 | translate 57 | (sin (t / offset) * 200.0 + 400.0) 58 | (cos (t / (offset + 50.0)) * 200.0 + 400.0) 59 | (scale' (mids fft / 10.0 + 0.05) 60 | (filled Data.Monoid.mempty shape)) 61 | --------------------------------------------------------------------------------