├── 001_empty ├── index.html └── sketch.js ├── 002_points ├── index.html └── sketch.js ├── 003_message ├── index.html └── sketch.js ├── 004_sounds ├── index.html ├── sketch.js └── sz.wav ├── 005_mask ├── index.html ├── mask.png └── sketch.js ├── 006_elements ├── eye.png ├── index.html ├── mouth.png ├── nose.png └── sketch.js ├── 007_scene ├── index.html ├── orccatgif.gif └── sketch.js ├── 008_emotion ├── index.html └── sketch.js ├── 009_pose-basic ├── index.html └── sketch.js ├── 010_pose-scene ├── data │ └── airplanes │ │ ├── 000.png │ │ ├── 001.png │ │ ├── 002.png │ │ ├── 003.png │ │ ├── 004.png │ │ ├── 005.png │ │ ├── 006.png │ │ ├── 007.png │ │ ├── 008.png │ │ └── 009.png ├── index.html └── sketch.js ├── README.md └── libs ├── clmtrackr.js ├── emotion_classifier.js ├── emotionmodel.js ├── ml5.min.js ├── model_pca_20_svm.js ├── p5.dom.js ├── p5.gif.js ├── p5.js ├── p5.sound.js ├── toxiclibs.min.js └── workshop-utils.js /001_empty/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /001_empty/sketch.js: -------------------------------------------------------------------------------- 1 | // note: see ../libs/workshop-utils.js for custom functions made for the workshop 2 | 3 | 4 | function setup() { 5 | 6 | } 7 | 8 | function draw() { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /002_points/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /002_points/sketch.js: -------------------------------------------------------------------------------- 1 | function setup() { 2 | loadCamera(); 3 | loadTracker(); 4 | loadCanvas(400,300); 5 | } 6 | 7 | function draw() { 8 | getPositions(); 9 | 10 | clear(); 11 | 12 | noStroke(); 13 | fill(255,150); 14 | rect(0,0,width,height); 15 | 16 | drawPoints(); 17 | } 18 | 19 | function drawPoints() { 20 | for (var i=0; i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /003_message/sketch.js: -------------------------------------------------------------------------------- 1 | function setup() { 2 | loadCamera(); 3 | loadTracker(); 4 | loadCanvas(400,400); 5 | } 6 | 7 | function draw() { 8 | getPositions(); 9 | 10 | background(0); 11 | 12 | textSize(20); 13 | textAlign(CENTER,CENTER); 14 | 15 | if(positions.length > 0) { 16 | // face is present 17 | fill(100,225,220); 18 | text("YOU ARE BEAUTIFUL", width/2, height/2); 19 | } else { 20 | fill(255); 21 | text("NOBODY IS WATCHING YOU", width/2, height/2); 22 | } 23 | } -------------------------------------------------------------------------------- /004_sounds/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /004_sounds/sketch.js: -------------------------------------------------------------------------------- 1 | var mySound; 2 | var canplay = true; 3 | 4 | function setup() { 5 | loadCamera(); 6 | loadTracker(); 7 | loadCanvas(400, 300); 8 | 9 | // load a sound 10 | mySound = loadSound("sz.wav"); 11 | } 12 | 13 | function draw() { 14 | getPositions(); 15 | 16 | clear(); 17 | 18 | noStroke(); 19 | fill(0,180); 20 | rect(0,0,width,height); 21 | 22 | if(positions.length > 0) { 23 | if(canplay) { 24 | mySound.play(); 25 | } 26 | canplay = false; 27 | } else { 28 | canplay = true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /004_sounds/sz.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/004_sounds/sz.wav -------------------------------------------------------------------------------- /005_mask/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /005_mask/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/005_mask/mask.png -------------------------------------------------------------------------------- /005_mask/sketch.js: -------------------------------------------------------------------------------- 1 | var myImage; 2 | 3 | function setup() { 4 | loadCamera(); 5 | loadTracker(); 6 | loadCanvas(400,300); 7 | 8 | // load an image 9 | myImage = loadImage("mask.png"); 10 | } 11 | 12 | function draw() { 13 | getPositions(); 14 | clear(); 15 | drawMask(); 16 | } 17 | 18 | function drawMask() { 19 | if(positions.length > 0) { 20 | var p1 = createVector(positions[7][0], positions[7][1] ); 21 | var p2 = createVector(positions[33][0], positions[33][1] ); 22 | 23 | stroke(255,0,0); 24 | line(p1.x,p1.y,p2.x, p2.y); 25 | 26 | // angle in radians 27 | var angleRad = Math.atan2(p2.y - p1.y, p2.x - p1.x); 28 | 29 | translate(positions[37][0], positions[37][1]); 30 | rotate(angleRad + PI/2); 31 | imageMode(CENTER,CENTER); 32 | 33 | var mSize = p1.dist(p2); 34 | image(myImage, 0, 0, mSize * 2.5, mSize * 2.5); 35 | } 36 | } -------------------------------------------------------------------------------- /006_elements/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/006_elements/eye.png -------------------------------------------------------------------------------- /006_elements/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /006_elements/mouth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/006_elements/mouth.png -------------------------------------------------------------------------------- /006_elements/nose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/006_elements/nose.png -------------------------------------------------------------------------------- /006_elements/sketch.js: -------------------------------------------------------------------------------- 1 | var eyeImage, noseImage, mouthImage; 2 | 3 | function setup() { 4 | loadCamera(); 5 | loadTracker(); 6 | loadCanvas(400,300); 7 | 8 | eyeImage = loadImage("eye.png"); 9 | noseImage = loadImage("nose.png"); 10 | mouthImage = loadImage("mouth.png"); 11 | } 12 | 13 | function draw() { 14 | getPositions(); 15 | clear(); 16 | drawElements(); 17 | } 18 | 19 | function drawElements() { 20 | if(positions.length > 0) { 21 | var p1 = createVector(positions[7][0], positions[7][1] ); 22 | var p2 = createVector(positions[33][0], positions[33][1] ); 23 | 24 | var eye1pos = createVector(positions[27][0],positions[27][1]); 25 | var eye2pos = createVector(positions[32][0],positions[32][1]); 26 | var nosepos = createVector(positions[41][0],positions[41][1]); 27 | var mouthpos = createVector(positions[57][0],positions[57][1]); 28 | 29 | stroke(255,0,0); 30 | line(p1.x,p1.y,p2.x, p2.y); 31 | 32 | noFill(); 33 | 34 | stroke(0,255,0); 35 | ellipse(eye1pos.x,eye1pos.y,10,10); 36 | ellipse(eye2pos.x, eye2pos.y,10,10); 37 | 38 | stroke(0,0,255); 39 | ellipse(nosepos.x, nosepos.y,5,5); 40 | ellipse(mouthpos.x, mouthpos.y,5,5); 41 | 42 | // angle in radians 43 | var angleRad = Math.atan2(p2.y - p1.y, p2.x - p1.x); 44 | var mSize = p1.dist(p2); 45 | 46 | imageMode(CENTER); 47 | 48 | push(); 49 | translate(eye1pos.x,eye1pos.y); 50 | rotate(angleRad + PI/2); 51 | image(eyeImage,0,0,mSize/2,mSize/2); 52 | pop(); 53 | 54 | push(); 55 | translate(eye2pos.x,eye2pos.y); 56 | rotate(angleRad + PI/2); 57 | image(eyeImage,0,0,mSize/2,mSize/2); 58 | pop(); 59 | 60 | push(); 61 | translate(mouthpos.x,mouthpos.y); 62 | rotate(angleRad + PI/2); 63 | image(mouthImage,0,0,mSize,mSize); 64 | pop(); 65 | 66 | push(); 67 | translate(nosepos.x,nosepos.y+10); 68 | rotate(angleRad + PI/2); 69 | image(noseImage,0,0,mSize,mSize); 70 | pop(); 71 | } 72 | } -------------------------------------------------------------------------------- /007_scene/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /007_scene/orccatgif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/007_scene/orccatgif.gif -------------------------------------------------------------------------------- /007_scene/sketch.js: -------------------------------------------------------------------------------- 1 | var myImage; 2 | var headpos; 3 | 4 | function setup() { 5 | loadCamera(); 6 | loadTracker(); 7 | loadCanvas(400,300); 8 | 9 | // load an image 10 | myGif = loadImage("orccatgif.gif"); 11 | } 12 | 13 | function draw() { 14 | getPositions(); 15 | 16 | background(179, 224, 255); 17 | 18 | 19 | if(positions.length > 0) { 20 | headpos = width/2 - positions[37][0]; 21 | } else { 22 | headpos = 0; 23 | } 24 | 25 | imageMode(CENTER); 26 | image(myGif,width/2,height/2); 27 | 28 | noStroke(); 29 | fill(77, 184, 255); 30 | rect(-headpos * 2 + width/4,0,width/2,height); 31 | 32 | } -------------------------------------------------------------------------------- /008_emotion/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /008_emotion/sketch.js: -------------------------------------------------------------------------------- 1 | function setup() { 2 | loadCamera(); 3 | loadTracker(); 4 | loadCanvas(400,300); 5 | } 6 | 7 | function draw() { 8 | getPositions(); 9 | getEmotions(); 10 | 11 | clear(); 12 | 13 | noStroke(); 14 | fill(0,150); 15 | rect(0,0,width,height); 16 | 17 | drawPoints(); 18 | 19 | if (emotions) { 20 | // andry=0, sad=1, surprised=2, happy=3 21 | for (var i = 0;i < predictedEmotions.length;i++) { 22 | rect(i * 110+20, height-80, 30, -predictedEmotions[i].value * 30); 23 | } 24 | } 25 | 26 | text("ANGRY", 20, height-40); 27 | text("SAD", 130, height-40); 28 | text("SURPRISED", 220, height-40); 29 | text("HAPPY", 340, height-40); 30 | 31 | } 32 | 33 | function drawPoints() { 34 | fill(255); 35 | for (var i=0; i 2 | 3 | 4 | Pose Basic 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /009_pose-basic/sketch.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Example taken from ml5js repository: 4 | https://github.com/ml5js/ml5-examples/tree/master/p5js/PoseNet 5 | 6 | */ 7 | 8 | let w = 640; 9 | let h = 480; 10 | let video; 11 | let poseNet; 12 | let poses = []; 13 | let skeletons = []; 14 | 15 | function setup() { 16 | createCanvas(w, h); 17 | video = createCapture(VIDEO); 18 | 19 | poseNet = ml5.poseNet(video, modelLoaded); 20 | 21 | poseNet.on('pose', function (results) { 22 | poses = results; 23 | }); 24 | 25 | video.hide(); 26 | fill(255); 27 | stroke(255); 28 | } 29 | 30 | function draw() { 31 | image(video, 0, 0, w, h); 32 | drawKeypoints(); 33 | drawSkeleton(); 34 | } 35 | 36 | 37 | 38 | function drawSkeleton() { 39 | for(let i = 0; i < poses.length; i++) { 40 | for(let j = 0; j < poses[i].skeleton.length; j++) { 41 | let partA = poses[i].skeleton[j][0]; 42 | let partB = poses[i].skeleton[j][1]; 43 | line(partA.position.x, partA.position.y, partB.position.x, partB.position.y); 44 | } 45 | } 46 | } 47 | 48 | function drawKeypoints() { 49 | for(let i = 0; i < poses.length; i++) { 50 | for(let j = 0; j < poses[i].pose.keypoints.length; j++) { 51 | let keypoint = poses[i].pose.keypoints[j]; 52 | if (keypoint.score > 0.2) { 53 | ellipse(keypoint.position.x, keypoint.position.y, 10, 10); 54 | } 55 | } 56 | } 57 | } 58 | 59 | function modelLoaded() { 60 | print('model loaded'); 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/000.png -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/001.png -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/002.png -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/003.png -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/004.png -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/005.png -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/006.png -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/007.png -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/008.png -------------------------------------------------------------------------------- /010_pose-scene/data/airplanes/009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stc/face-tracking-p5js/4d72f029a2cf1a6ad200b1361f5c6794332061a7/010_pose-scene/data/airplanes/009.png -------------------------------------------------------------------------------- /010_pose-scene/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pose Scene 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /010_pose-scene/sketch.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | A scene where multiple participants are involved in the interaction 4 | todo: add multiple head positions 5 | 6 | */ 7 | 8 | let w = 640; 9 | let h = 480; 10 | let video; 11 | let poseNet; 12 | let poses = []; 13 | let skeletons = []; 14 | let textures = []; 15 | let pPositions = []; 16 | let cPositions = []; 17 | let xs = []; 18 | let easing = 0.1; 19 | 20 | // physics for playful interaction 21 | let VerletPhysics2D = toxi.physics2d.VerletPhysics2D, 22 | VerletParticle2D = toxi.physics2d.VerletParticle2D, 23 | AttractionBehavior = toxi.physics2d.behaviors.AttractionBehavior, 24 | GravityBehavior = toxi.physics2d.behaviors.GravityBehavior, 25 | Vec2D = toxi.geom.Vec2D, 26 | Rect = toxi.geom.Rect; 27 | 28 | let NUM_PARTICLES = 10; 29 | 30 | let physics; 31 | let mouseAttractor; 32 | let mousePos; 33 | 34 | let headAttractor; 35 | let headPos; 36 | 37 | let leftSAttractor; 38 | let leftPos; 39 | 40 | let rightSAttractor; 41 | let rightPos; 42 | 43 | let leftHAttractor; 44 | let leftHPos; 45 | 46 | let rightHAttractor; 47 | let rightHPos; 48 | 49 | function setup() { 50 | createCanvas(w, h); 51 | video = createCapture(VIDEO); 52 | loadTextures(); 53 | for(let i=0;i 0.2) { 159 | fill(255,0,100); 160 | noStroke(); 161 | ellipse(keypoint.position.x, keypoint.position.y, 10, 10); 162 | text(j,keypoint.position.x+10, keypoint.position.y); 163 | if(j==0) { 164 | headPos.set(keypoint.position.x, keypoint.position.y); 165 | noFill(); 166 | stroke(100,100,0); 167 | ellipse(keypoint.position.x, keypoint.position.y,200,200); 168 | } 169 | if(j==5) { 170 | leftPos.set(keypoint.position.x, keypoint.position.y); 171 | noFill(); 172 | stroke(100,100,0); 173 | ellipse(keypoint.position.x, keypoint.position.y,100,100); 174 | } 175 | if(j==6) { 176 | rightPos.set(keypoint.position.x, keypoint.position.y); 177 | noFill(); 178 | stroke(100,100,0); 179 | ellipse(keypoint.position.x, keypoint.position.y,100,100); 180 | } 181 | 182 | if(j==9) { 183 | rightHPos.set(keypoint.position.x, keypoint.position.y); 184 | noFill(); 185 | stroke(100,100,0); 186 | ellipse(keypoint.position.x, keypoint.position.y,100,100); 187 | } 188 | 189 | if(j==10) { 190 | leftHPos.set(keypoint.position.x, keypoint.position.y); 191 | noFill(); 192 | stroke(100,100,0); 193 | ellipse(keypoint.position.x, keypoint.position.y,100,100); 194 | } 195 | 196 | } 197 | } 198 | } 199 | } 200 | 201 | function loadTextures() { 202 | for(let i=0;i<10;i++) { 203 | let myTexture = loadImage("data/airplanes/00" + i + ".png"); 204 | textures.push(myTexture); 205 | } 206 | } 207 | 208 | function modelLoaded() { 209 | print('model loaded'); 210 | } 211 | 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Face Tracking for Creative Coding 2 | ### Overview 3 | A growing collection of examples used to demonstrate basic concepts of the usage of face tracking with creative coding on the modern web. This repository is the basis of the workshop series called _DataFaces_. These events are focusing on data politics, creative coding, machine learning and critical discussion on cutting edge (surveillance based) technologies, the concepts are usually introduced in a playful & practical way using open source coding frameworks. 4 | 5 | ![001](https://user-images.githubusercontent.com/270431/40185787-c0e67830-59f3-11e8-9b94-5e619b195f57.jpg) 6 | _fig. 1: Feature points of tracked faces_ 7 | 8 | ![004inverted](https://user-images.githubusercontent.com/270431/40185873-0b663e18-59f4-11e8-94b8-250c339d3813.jpg) 9 | _fig. 2: Left- Source Code in Brackets editor, Middle- Indexed feature points, Right- Error message_ 10 | 11 | ### Installation 12 | All the examples are using [P5JS](http://p5js.org/) for displaying graphics and playing back sounds. The face tracking is based on [clmtrackr](https://github.com/auduno/clmtrackr), except for _009_pose-basic_ & _010_pose_scene_ examples, see below for details. Each of these libraries can be found in the 'libs' folder. To launch the experiments, download the [brackets](http://brackets.io/) editor. Open the folder of this repository and hit 'live preview' on the top right. 13 | 14 | ![003](https://user-images.githubusercontent.com/270431/40185938-32323722-59f4-11e8-98a0-0bb116c9bf3c.jpg) 15 | _fig. 3: Scene example, where graphical elements are moved, based on head position_ 16 | 17 | ![002](https://user-images.githubusercontent.com/270431/40185998-5080e4f8-59f4-11e8-879a-97ec2d30deef.jpg) 18 | _fig. 4: Sentiment Analysis_ 19 | 20 | ### Folder Structure 21 | Each example includes media files (sounds, animated gifs, images etc). The 'libs' folder contains the necessary js libraries and the pre-trained face traclking models. The examples are kept as simple as possible, so some html related manipulation needed to be wrapped into some utility functions. Check [libs/worshop-utils.js](https://github.com/stc/face-tracking-p5js/blob/master/libs/workshop-utils.js) on how these simplified, workshop specific functions operate (loading camera, tracker, etc.) 22 | 23 | ### Workshop Schedule 24 | See the [wiki](https://github.com/stc/face-tracking-p5js/wiki/Schedule) for detailed workshop schedule and the specific tasks we are dealing with during the session. Durations of the parts may vary depending on the knowledge and interest of the participants. 25 | 26 | ### Notes 27 | 28 | 29 | 30 | _009_pose-basic_ & _010_pose_scene_ examples are using [pose estimation from tensorflow.js](https://github.com/tensorflow/tfjs-models/tree/master/posenet) together with p5js (we are using it for multiple head tracking on the workshop). This version of PoseNet is built with the [ml5 javascript libary](https://ml5js.org/), that aims to make machine learning accessible to a broad audience of artists, creative coders, and students. The library provides access to machine learning algorithms and models in the browser, building on top of TensorFlow.js with no other external dependencies. 31 | 32 | 33 | (c) 2018 Agoston Nagy / gpl v3 34 | 35 | -------------------------------------------------------------------------------- /libs/emotion_classifier.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var emotionClassifier = function() { 3 | 4 | var previousParameters = []; 5 | var classifier = {}; 6 | var emotions = []; 7 | var coefficient_length; 8 | 9 | this.getEmotions = function() { 10 | return emotions; 11 | } 12 | 13 | this.init = function(model) { 14 | // load it 15 | for (var m in model) { 16 | emotions.push(m); 17 | classifier[m] = {}; 18 | classifier[m]['bias'] = model[m]['bias']; 19 | classifier[m]['coefficients'] = model[m]['coefficients']; 20 | } 21 | coefficient_length = classifier[emotions[0]]['coefficients'].length; 22 | } 23 | 24 | this.getBlank = function() { 25 | var prediction = []; 26 | for (var j = 0;j < emotions.length;j++) { 27 | prediction[j] = {"emotion" : emotions[j], "value" : 0.0}; 28 | } 29 | return prediction; 30 | } 31 | 32 | this.predict = function(parameters) { 33 | var prediction = []; 34 | for (var j = 0;j < emotions.length;j++) { 35 | var e = emotions[j]; 36 | var score = classifier[e].bias; 37 | for (var i = 0;i < coefficient_length;i++) { 38 | score += classifier[e].coefficients[i]*parameters[i+6]; 39 | } 40 | prediction[j] = {"emotion" : e, "value" : 0.0}; 41 | prediction[j]['value'] = 1.0/(1.0 + Math.exp(-score)); 42 | } 43 | return prediction; 44 | } 45 | 46 | this.meanPredict = function (parameters) { 47 | // store to array of 10 previous parameters 48 | previousParameters.splice(0, previousParameters.length == 10 ? 1 : 0); 49 | previousParameters.push(parameters.slice(0)); 50 | 51 | if (previousParameters.length > 9) { 52 | // calculate mean of parameters? 53 | var meanParameters = []; 54 | for (var i = 0;i < parameters.length;i++) { 55 | meanParameters[i] = 0; 56 | } 57 | for (var i = 0;i < previousParameters.length;i++) { 58 | for (var j = 0;j < parameters.length;j++) { 59 | meanParameters[j] += previousParameters[i][j]; 60 | } 61 | } 62 | for (var i = 0;i < parameters.length;i++) { 63 | meanParameters[i] /= 10; 64 | } 65 | 66 | // calculate logistic regression 67 | return this.predict(meanParameters); 68 | } else { 69 | return false; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /libs/emotionmodel.js: -------------------------------------------------------------------------------- 1 | var emotionModel = { 2 | "angry" : { 3 | "bias" : -2.3768163629, 4 | "coefficients" : [-0.026270300474413848, 0.037963304603547195, 0.25318394482150264, -0.36801694354709802, -0.059638621925431838, 6.3145056900010567e-17, 0.094520059272651849, 0.21347244366388901, 0.42885313652690621, -1.5592214434343613e-14, 0.13850079872874066, -5.1485910666665307e-16, 0.33298910350203975, 8.0357363919330235e-16, 0.0025325096363696059, -0.44615090964065951, -1.5784656134660036e-15, 0.047596008125675944], 5 | }, 6 | "disgusted" : { 7 | "bias" : -2.27900176842, 8 | "coefficients" : [0.042360511043701296, 0.1282033922181087, 0.12391812407152457, -0.27567823277270387, 0.1421150306247343, -3.1081856766624292e-16, 0.12612972927139088, 0.23426310789552218, 0.058894842405560956, -4.0618311657856847e-15, 0.22340906131116328, -5.81584759084207e-15, 0.25735218595500009, 1.3658078149815552e-15, -0.12506850140605949, -0.9463447584787309, -4.555025133881674e-15, 0.07141679477545719], 9 | }, 10 | "fear" : { 11 | "bias" : -3.40339917096, 12 | "coefficients" : [-0.1484066846778026, -0.090079860583144475, -0.16138464891003612, 0.078750143250805593, 0.070521075864349317, 3.6426173865029096e-14, 0.54106033239630258, 0.049586639890528791, -0.10793093750863458, -5.1279691693889055e-15, -0.092243236155683667, -1.5165430767377168e-14, 0.19842076279793416, 3.8282960479670228e-15, -0.67367184030514637, -0.2166709100861198, 1.1995348541944584e-14, -0.20024153378658624], 13 | }, 14 | "sad" : { 15 | "bias" : -2.75274632938, 16 | "coefficients" : [0.092611010001705449, -0.12883530427748521, -0.068975994604949298, -0.19623077060801897, 0.055312197843294802, -3.5874521027522394e-16, 0.46315306348086854, -0.32103622843654361, -0.46536626891885491, 1.725612051187888e-14, -0.40841535552399683, 2.1258665882389598e-14, 0.45405204011625938, 5.9194289392226669e-15, 0.094410500655151899, -0.4861387223131064, -3.030330454831321e-15, 0.73708254653765559], 17 | }, 18 | "surprised" : { 19 | "bias" : -2.86262062696, 20 | "coefficients" : [-0.12854109490879712, -0.049194392540246726, -0.22856553950573175, 0.2992140056765602, -0.25975558754705375, 1.4688408313649554e-09, -0.13685597104348368, -0.23581884244542603, 0.026447180058097462, 1.6822695398601112e-10, 0.095712304864421921, -4.4670230074132591e-10, 0.40505706085269738, 2.7821987602166784e-11, -0.54367856543588833, -0.096320945782919151, 1.4239801195516449e-10, -0.7238167998685946], 21 | }, 22 | "happy" : { 23 | "bias" : -1.4786712822, 24 | "coefficients" : [0.014837209675880276, 0.31092627456808286, -0.1214238695400552, 0.45265837869400843, 0.36700140259900887, 1.7113646510738279e-15, -0.4786251427206033, -0.15377369505521801, -0.16948121903942992, 6.0300272629176253e-15, -0.021184992727875669, -6.9318606881292957e-15, -0.81611603551588852, -3.7883560238442657e-15, 0.1486320646217055, 0.94973410351769527, 3.6260400713070416e-15, -0.31361179943007411], 25 | }, 26 | }; -------------------------------------------------------------------------------- /libs/p5.dom.js: -------------------------------------------------------------------------------- 1 | /*! p5.dom.js v0.4.0 August 9, 2018 */ 2 | /** 3 | *

The web is much more than just canvas and p5.dom makes it easy to interact 4 | * with other HTML5 objects, including text, hyperlink, image, input, video, 5 | * audio, and webcam.

6 | *

There is a set of creation methods, DOM manipulation methods, and 7 | * an extended p5.Element that supports a range of HTML elements. See the 8 | * 9 | * beyond the canvas tutorial for a full overview of how this addon works. 10 | * 11 | *

Methods and properties shown in black are part of the p5.js core, items in 12 | * blue are part of the p5.dom library. You will need to include an extra file 13 | * in order to access the blue functions. See the 14 | * using a library 15 | * section for information on how to include this library. p5.dom comes with 16 | * p5 complete or you can download the single file 17 | * 18 | * here.

19 | *

See tutorial: beyond the canvas 20 | * for more info on how to use this library. 21 | * 22 | * @module p5.dom 23 | * @submodule p5.dom 24 | * @for p5 25 | * @main 26 | */ 27 | 28 | (function(root, factory) { 29 | if (typeof define === 'function' && define.amd) 30 | define('p5.dom', ['p5'], function(p5) { 31 | factory(p5); 32 | }); 33 | else if (typeof exports === 'object') factory(require('../p5')); 34 | else factory(root['p5']); 35 | })(this, function(p5) { 36 | // ============================================================================= 37 | // p5 additions 38 | // ============================================================================= 39 | 40 | /** 41 | * Searches the page for an element with the given ID, class, or tag name (using the '#' or '.' 42 | * prefixes to specify an ID or class respectively, and none for a tag) and returns it as 43 | * a p5.Element. If a class or tag name is given with more than 1 element, 44 | * only the first element will be returned. 45 | * The DOM node itself can be accessed with .elt. 46 | * Returns null if none found. You can also specify a container to search within. 47 | * 48 | * @method select 49 | * @param {String} name id, class, or tag name of element to search for 50 | * @param {String|p5.Element|HTMLElement} [container] id, p5.Element, or 51 | * HTML element to search within 52 | * @return {p5.Element|null} p5.Element containing node found 53 | * @example 54 | *

55 | * function setup() { 56 | * createCanvas(100, 100); 57 | * //translates canvas 50px down 58 | * select('canvas').position(100, 100); 59 | * } 60 | *
61 | *
62 | * // these are all valid calls to select() 63 | * var a = select('#moo'); 64 | * var b = select('#blah', '#myContainer'); 65 | * var c, e; 66 | * if (b) { 67 | * c = select('#foo', b); 68 | * } 69 | * var d = document.getElementById('beep'); 70 | * if (d) { 71 | * e = select('p', d); 72 | * } 73 | * [a, b, c, d, e]; // unused 74 | *
75 | * 76 | */ 77 | p5.prototype.select = function(e, p) { 78 | p5._validateParameters('select', arguments); 79 | var res = null; 80 | var container = getContainer(p); 81 | if (e[0] === '.') { 82 | e = e.slice(1); 83 | res = container.getElementsByClassName(e); 84 | if (res.length) { 85 | res = res[0]; 86 | } else { 87 | res = null; 88 | } 89 | } else if (e[0] === '#') { 90 | e = e.slice(1); 91 | res = container.getElementById(e); 92 | } else { 93 | res = container.getElementsByTagName(e); 94 | if (res.length) { 95 | res = res[0]; 96 | } else { 97 | res = null; 98 | } 99 | } 100 | if (res) { 101 | return this._wrapElement(res); 102 | } else { 103 | return null; 104 | } 105 | }; 106 | 107 | /** 108 | * Searches the page for elements with the given class or tag name (using the '.' prefix 109 | * to specify a class and no prefix for a tag) and returns them as p5.Elements 110 | * in an array. 111 | * The DOM node itself can be accessed with .elt. 112 | * Returns an empty array if none found. 113 | * You can also specify a container to search within. 114 | * 115 | * @method selectAll 116 | * @param {String} name class or tag name of elements to search for 117 | * @param {String} [container] id, p5.Element, or HTML element to search within 118 | * @return {p5.Element[]} Array of p5.Elements containing nodes found 119 | * @example 120 | *
121 | * function setup() { 122 | * createButton('btn'); 123 | * createButton('2nd btn'); 124 | * createButton('3rd btn'); 125 | * var buttons = selectAll('button'); 126 | * 127 | * for (var i = 0; i < buttons.length; i++) { 128 | * buttons[i].size(100, 100); 129 | * } 130 | * } 131 | *
132 | *
133 | * // these are all valid calls to selectAll() 134 | * var a = selectAll('.moo'); 135 | * a = selectAll('div'); 136 | * a = selectAll('button', '#myContainer'); 137 | * 138 | * var d = select('#container'); 139 | * a = selectAll('p', d); 140 | * 141 | * var f = document.getElementById('beep'); 142 | * a = select('.blah', f); 143 | * 144 | * a; // unused 145 | *
146 | * 147 | */ 148 | p5.prototype.selectAll = function(e, p) { 149 | p5._validateParameters('selectAll', arguments); 150 | var arr = []; 151 | var res; 152 | var container = getContainer(p); 153 | if (e[0] === '.') { 154 | e = e.slice(1); 155 | res = container.getElementsByClassName(e); 156 | } else { 157 | res = container.getElementsByTagName(e); 158 | } 159 | if (res) { 160 | for (var j = 0; j < res.length; j++) { 161 | var obj = this._wrapElement(res[j]); 162 | arr.push(obj); 163 | } 164 | } 165 | return arr; 166 | }; 167 | 168 | /** 169 | * Helper function for select and selectAll 170 | */ 171 | function getContainer(p) { 172 | var container = document; 173 | if (typeof p === 'string' && p[0] === '#') { 174 | p = p.slice(1); 175 | container = document.getElementById(p) || document; 176 | } else if (p instanceof p5.Element) { 177 | container = p.elt; 178 | } else if (p instanceof HTMLElement) { 179 | container = p; 180 | } 181 | return container; 182 | } 183 | 184 | /** 185 | * Helper function for getElement and getElements. 186 | */ 187 | p5.prototype._wrapElement = function(elt) { 188 | var children = Array.prototype.slice.call(elt.children); 189 | if (elt.tagName === 'INPUT' && elt.type === 'checkbox') { 190 | var converted = new p5.Element(elt, this); 191 | converted.checked = function() { 192 | if (arguments.length === 0) { 193 | return this.elt.checked; 194 | } else if (arguments[0]) { 195 | this.elt.checked = true; 196 | } else { 197 | this.elt.checked = false; 198 | } 199 | return this; 200 | }; 201 | return converted; 202 | } else if (elt.tagName === 'VIDEO' || elt.tagName === 'AUDIO') { 203 | return new p5.MediaElement(elt, this); 204 | } else if (elt.tagName === 'SELECT') { 205 | return this.createSelect(new p5.Element(elt, this)); 206 | } else if ( 207 | children.length > 0 && 208 | children.every(function(c) { 209 | return c.tagName === 'INPUT' || c.tagName === 'LABEL'; 210 | }) 211 | ) { 212 | return this.createRadio(new p5.Element(elt, this)); 213 | } else { 214 | return new p5.Element(elt, this); 215 | } 216 | }; 217 | 218 | /** 219 | * Removes all elements created by p5, except any canvas / graphics 220 | * elements created by createCanvas or createGraphics. 221 | * Event handlers are removed, and element is removed from the DOM. 222 | * @method removeElements 223 | * @example 224 | *
225 | * function setup() { 226 | * createCanvas(100, 100); 227 | * createDiv('this is some text'); 228 | * createP('this is a paragraph'); 229 | * } 230 | * function mousePressed() { 231 | * removeElements(); // this will remove the div and p, not canvas 232 | * } 233 | *
234 | * 235 | */ 236 | p5.prototype.removeElements = function(e) { 237 | p5._validateParameters('removeElements', arguments); 238 | for (var i = 0; i < this._elements.length; i++) { 239 | if (!(this._elements[i].elt instanceof HTMLCanvasElement)) { 240 | this._elements[i].remove(); 241 | } 242 | } 243 | }; 244 | 245 | /** 246 | * The .changed() function is called when the value of an 247 | * element changes. 248 | * This can be used to attach an element specific event listener. 249 | * 250 | * @method changed 251 | * @param {Function|Boolean} fxn function to be fired when the value of 252 | * an element changes. 253 | * if `false` is passed instead, the previously 254 | * firing function will no longer fire. 255 | * @chainable 256 | * @example 257 | *
258 | * var sel; 259 | * 260 | * function setup() { 261 | * textAlign(CENTER); 262 | * background(200); 263 | * sel = createSelect(); 264 | * sel.position(10, 10); 265 | * sel.option('pear'); 266 | * sel.option('kiwi'); 267 | * sel.option('grape'); 268 | * sel.changed(mySelectEvent); 269 | * } 270 | * 271 | * function mySelectEvent() { 272 | * var item = sel.value(); 273 | * background(200); 274 | * text("it's a " + item + '!', 50, 50); 275 | * } 276 | *
277 | * 278 | *
279 | * var checkbox; 280 | * var cnv; 281 | * 282 | * function setup() { 283 | * checkbox = createCheckbox(' fill'); 284 | * checkbox.changed(changeFill); 285 | * cnv = createCanvas(100, 100); 286 | * cnv.position(0, 30); 287 | * noFill(); 288 | * } 289 | * 290 | * function draw() { 291 | * background(200); 292 | * ellipse(50, 50, 50, 50); 293 | * } 294 | * 295 | * function changeFill() { 296 | * if (checkbox.checked()) { 297 | * fill(0); 298 | * } else { 299 | * noFill(); 300 | * } 301 | * } 302 | *
303 | * 304 | * @alt 305 | * dropdown: pear, kiwi, grape. When selected text "its a" + selection shown. 306 | * 307 | */ 308 | p5.Element.prototype.changed = function(fxn) { 309 | p5.Element._adjustListener('change', fxn, this); 310 | return this; 311 | }; 312 | 313 | /** 314 | * The .input() function is called when any user input is 315 | * detected with an element. The input event is often used 316 | * to detect keystrokes in a input element, or changes on a 317 | * slider element. This can be used to attach an element specific 318 | * event listener. 319 | * 320 | * @method input 321 | * @param {Function|Boolean} fxn function to be fired when any user input is 322 | * detected within the element. 323 | * if `false` is passed instead, the previously 324 | * firing function will no longer fire. 325 | * @chainable 326 | * @example 327 | *
328 | * // Open your console to see the output 329 | * function setup() { 330 | * var inp = createInput(''); 331 | * inp.input(myInputEvent); 332 | * } 333 | * 334 | * function myInputEvent() { 335 | * console.log('you are typing: ', this.value()); 336 | * } 337 | *
338 | * 339 | * @alt 340 | * no display. 341 | * 342 | */ 343 | p5.Element.prototype.input = function(fxn) { 344 | p5.Element._adjustListener('input', fxn, this); 345 | return this; 346 | }; 347 | 348 | /** 349 | * Helpers for create methods. 350 | */ 351 | function addElement(elt, pInst, media) { 352 | var node = pInst._userNode ? pInst._userNode : document.body; 353 | node.appendChild(elt); 354 | var c = media 355 | ? new p5.MediaElement(elt, pInst) 356 | : new p5.Element(elt, pInst); 357 | pInst._elements.push(c); 358 | return c; 359 | } 360 | 361 | /** 362 | * Creates a <div></div> element in the DOM with given inner HTML. 363 | * Appends to the container node if one is specified, otherwise 364 | * appends to body. 365 | * 366 | * @method createDiv 367 | * @param {String} [html] inner HTML for element created 368 | * @return {p5.Element} pointer to p5.Element holding created node 369 | * @example 370 | *
371 | * createDiv('this is some text'); 372 | *
373 | */ 374 | 375 | /** 376 | * Creates a <p></p> element in the DOM with given inner HTML. Used 377 | * for paragraph length text. 378 | * Appends to the container node if one is specified, otherwise 379 | * appends to body. 380 | * 381 | * @method createP 382 | * @param {String} [html] inner HTML for element created 383 | * @return {p5.Element} pointer to p5.Element holding created node 384 | * @example 385 | *
386 | * createP('this is some text'); 387 | *
388 | */ 389 | 390 | /** 391 | * Creates a <span></span> element in the DOM with given inner HTML. 392 | * Appends to the container node if one is specified, otherwise 393 | * appends to body. 394 | * 395 | * @method createSpan 396 | * @param {String} [html] inner HTML for element created 397 | * @return {p5.Element} pointer to p5.Element holding created node 398 | * @example 399 | *
400 | * createSpan('this is some text'); 401 | *
402 | */ 403 | var tags = ['div', 'p', 'span']; 404 | tags.forEach(function(tag) { 405 | var method = 'create' + tag.charAt(0).toUpperCase() + tag.slice(1); 406 | p5.prototype[method] = function(html) { 407 | var elt = document.createElement(tag); 408 | elt.innerHTML = typeof html === 'undefined' ? '' : html; 409 | return addElement(elt, this); 410 | }; 411 | }); 412 | 413 | /** 414 | * Creates an <img> element in the DOM with given src and 415 | * alternate text. 416 | * Appends to the container node if one is specified, otherwise 417 | * appends to body. 418 | * 419 | * @method createImg 420 | * @param {String} src src path or url for image 421 | * @param {String} [alt] alternate text to be used if image does not load 422 | * @param {Function} [successCallback] callback to be called once image data is loaded 423 | * @return {p5.Element} pointer to p5.Element holding created node 424 | * @example 425 | *
426 | * createImg('http://p5js.org/img/asterisk-01.png'); 427 | *
428 | */ 429 | /** 430 | * @method createImg 431 | * @param {String} src 432 | * @param {Function} successCallback 433 | * @return {Object|p5.Element} 434 | */ 435 | p5.prototype.createImg = function() { 436 | p5._validateParameters('createImg', arguments); 437 | var elt = document.createElement('img'); 438 | var args = arguments; 439 | var self; 440 | var setAttrs = function() { 441 | self.width = elt.offsetWidth || elt.width; 442 | self.height = elt.offsetHeight || elt.height; 443 | if (args.length > 1 && typeof args[1] === 'function') { 444 | self.fn = args[1]; 445 | self.fn(); 446 | } else if (args.length > 1 && typeof args[2] === 'function') { 447 | self.fn = args[2]; 448 | self.fn(); 449 | } 450 | }; 451 | elt.src = args[0]; 452 | if (args.length > 1 && typeof args[1] === 'string') { 453 | elt.alt = args[1]; 454 | } 455 | elt.onload = function() { 456 | setAttrs(); 457 | }; 458 | self = addElement(elt, this); 459 | return self; 460 | }; 461 | 462 | /** 463 | * Creates an <a></a> element in the DOM for including a hyperlink. 464 | * Appends to the container node if one is specified, otherwise 465 | * appends to body. 466 | * 467 | * @method createA 468 | * @param {String} href url of page to link to 469 | * @param {String} html inner html of link element to display 470 | * @param {String} [target] target where new link should open, 471 | * could be _blank, _self, _parent, _top. 472 | * @return {p5.Element} pointer to p5.Element holding created node 473 | * @example 474 | *
475 | * createA('http://p5js.org/', 'this is a link'); 476 | *
477 | */ 478 | p5.prototype.createA = function(href, html, target) { 479 | p5._validateParameters('createA', arguments); 480 | var elt = document.createElement('a'); 481 | elt.href = href; 482 | elt.innerHTML = html; 483 | if (target) elt.target = target; 484 | return addElement(elt, this); 485 | }; 486 | 487 | /** INPUT **/ 488 | 489 | /** 490 | * Creates a slider <input></input> element in the DOM. 491 | * Use .size() to set the display length of the slider. 492 | * Appends to the container node if one is specified, otherwise 493 | * appends to body. 494 | * 495 | * @method createSlider 496 | * @param {Number} min minimum value of the slider 497 | * @param {Number} max maximum value of the slider 498 | * @param {Number} [value] default value of the slider 499 | * @param {Number} [step] step size for each tick of the slider (if step is set to 0, the slider will move continuously from the minimum to the maximum value) 500 | * @return {p5.Element} pointer to p5.Element holding created node 501 | * @example 502 | *
503 | * var slider; 504 | * function setup() { 505 | * slider = createSlider(0, 255, 100); 506 | * slider.position(10, 10); 507 | * slider.style('width', '80px'); 508 | * } 509 | * 510 | * function draw() { 511 | * var val = slider.value(); 512 | * background(val); 513 | * } 514 | *
515 | * 516 | *
517 | * var slider; 518 | * function setup() { 519 | * colorMode(HSB); 520 | * slider = createSlider(0, 360, 60, 40); 521 | * slider.position(10, 10); 522 | * slider.style('width', '80px'); 523 | * } 524 | * 525 | * function draw() { 526 | * var val = slider.value(); 527 | * background(val, 100, 100, 1); 528 | * } 529 | *
530 | */ 531 | p5.prototype.createSlider = function(min, max, value, step) { 532 | p5._validateParameters('createSlider', arguments); 533 | var elt = document.createElement('input'); 534 | elt.type = 'range'; 535 | elt.min = min; 536 | elt.max = max; 537 | if (step === 0) { 538 | elt.step = 0.000000000000000001; // smallest valid step 539 | } else if (step) { 540 | elt.step = step; 541 | } 542 | if (typeof value === 'number') elt.value = value; 543 | return addElement(elt, this); 544 | }; 545 | 546 | /** 547 | * Creates a <button></button> element in the DOM. 548 | * Use .size() to set the display size of the button. 549 | * Use .mousePressed() to specify behavior on press. 550 | * Appends to the container node if one is specified, otherwise 551 | * appends to body. 552 | * 553 | * @method createButton 554 | * @param {String} label label displayed on the button 555 | * @param {String} [value] value of the button 556 | * @return {p5.Element} pointer to p5.Element holding created node 557 | * @example 558 | *
559 | * var button; 560 | * function setup() { 561 | * createCanvas(100, 100); 562 | * background(0); 563 | * button = createButton('click me'); 564 | * button.position(19, 19); 565 | * button.mousePressed(changeBG); 566 | * } 567 | * 568 | * function changeBG() { 569 | * var val = random(255); 570 | * background(val); 571 | * } 572 | *
573 | */ 574 | p5.prototype.createButton = function(label, value) { 575 | p5._validateParameters('createButton', arguments); 576 | var elt = document.createElement('button'); 577 | elt.innerHTML = label; 578 | if (value) elt.value = value; 579 | return addElement(elt, this); 580 | }; 581 | 582 | /** 583 | * Creates a checkbox <input></input> element in the DOM. 584 | * Calling .checked() on a checkbox returns if it is checked or not 585 | * 586 | * @method createCheckbox 587 | * @param {String} [label] label displayed after checkbox 588 | * @param {boolean} [value] value of the checkbox; checked is true, unchecked is false 589 | * @return {p5.Element} pointer to p5.Element holding created node 590 | * @example 591 | *
592 | * var checkbox; 593 | * 594 | * function setup() { 595 | * checkbox = createCheckbox('label', false); 596 | * checkbox.changed(myCheckedEvent); 597 | * } 598 | * 599 | * function myCheckedEvent() { 600 | * if (this.checked()) { 601 | * console.log('Checking!'); 602 | * } else { 603 | * console.log('Unchecking!'); 604 | * } 605 | * } 606 | *
607 | */ 608 | p5.prototype.createCheckbox = function() { 609 | p5._validateParameters('createCheckbox', arguments); 610 | var elt = document.createElement('div'); 611 | var checkbox = document.createElement('input'); 612 | checkbox.type = 'checkbox'; 613 | elt.appendChild(checkbox); 614 | //checkbox must be wrapped in p5.Element before label so that label appears after 615 | var self = addElement(elt, this); 616 | self.checked = function() { 617 | var cb = self.elt.getElementsByTagName('input')[0]; 618 | if (cb) { 619 | if (arguments.length === 0) { 620 | return cb.checked; 621 | } else if (arguments[0]) { 622 | cb.checked = true; 623 | } else { 624 | cb.checked = false; 625 | } 626 | } 627 | return self; 628 | }; 629 | this.value = function(val) { 630 | self.value = val; 631 | return this; 632 | }; 633 | if (arguments[0]) { 634 | var ran = Math.random() 635 | .toString(36) 636 | .slice(2); 637 | var label = document.createElement('label'); 638 | checkbox.setAttribute('id', ran); 639 | label.htmlFor = ran; 640 | self.value(arguments[0]); 641 | label.appendChild(document.createTextNode(arguments[0])); 642 | elt.appendChild(label); 643 | } 644 | if (arguments[1]) { 645 | checkbox.checked = true; 646 | } 647 | return self; 648 | }; 649 | 650 | /** 651 | * Creates a dropdown menu <select></select> element in the DOM. 652 | * It also helps to assign select-box methods to p5.Element when selecting existing select box 653 | * @method createSelect 654 | * @param {boolean} [multiple] true if dropdown should support multiple selections 655 | * @return {p5.Element} 656 | * @example 657 | *
658 | * var sel; 659 | * 660 | * function setup() { 661 | * textAlign(CENTER); 662 | * background(200); 663 | * sel = createSelect(); 664 | * sel.position(10, 10); 665 | * sel.option('pear'); 666 | * sel.option('kiwi'); 667 | * sel.option('grape'); 668 | * sel.changed(mySelectEvent); 669 | * } 670 | * 671 | * function mySelectEvent() { 672 | * var item = sel.value(); 673 | * background(200); 674 | * text('it is a' + item + '!', 50, 50); 675 | * } 676 | *
677 | */ 678 | /** 679 | * @method createSelect 680 | * @param {Object} existing DOM select element 681 | * @return {p5.Element} 682 | */ 683 | 684 | p5.prototype.createSelect = function() { 685 | p5._validateParameters('createSelect', arguments); 686 | var elt, self; 687 | var arg = arguments[0]; 688 | if (typeof arg === 'object' && arg.elt.nodeName === 'SELECT') { 689 | self = arg; 690 | elt = this.elt = arg.elt; 691 | } else { 692 | elt = document.createElement('select'); 693 | if (arg && typeof arg === 'boolean') { 694 | elt.setAttribute('multiple', 'true'); 695 | } 696 | self = addElement(elt, this); 697 | } 698 | self.option = function(name, value) { 699 | var index; 700 | //see if there is already an option with this name 701 | for (var i = 0; i < this.elt.length; i++) { 702 | if (this.elt[i].innerHTML === name) { 703 | index = i; 704 | break; 705 | } 706 | } 707 | //if there is an option with this name we will modify it 708 | if (index !== undefined) { 709 | //if the user passed in false then delete that option 710 | if (value === false) { 711 | this.elt.remove(index); 712 | } else { 713 | //otherwise if the name and value are the same then change both 714 | if (this.elt[index].innerHTML === this.elt[index].value) { 715 | this.elt[index].innerHTML = this.elt[index].value = value; 716 | //otherwise just change the value 717 | } else { 718 | this.elt[index].value = value; 719 | } 720 | } 721 | } else { 722 | //if it doesn't exist make it 723 | var opt = document.createElement('option'); 724 | opt.innerHTML = name; 725 | if (arguments.length > 1) opt.value = value; 726 | else opt.value = name; 727 | elt.appendChild(opt); 728 | } 729 | }; 730 | self.selected = function(value) { 731 | var arr = [], 732 | i; 733 | if (arguments.length > 0) { 734 | for (i = 0; i < this.elt.length; i++) { 735 | if (value.toString() === this.elt[i].value) { 736 | this.elt.selectedIndex = i; 737 | } 738 | } 739 | return this; 740 | } else { 741 | if (this.elt.getAttribute('multiple')) { 742 | for (i = 0; i < this.elt.selectedOptions.length; i++) { 743 | arr.push(this.elt.selectedOptions[i].value); 744 | } 745 | return arr; 746 | } else { 747 | return this.elt.value; 748 | } 749 | } 750 | }; 751 | return self; 752 | }; 753 | 754 | /** 755 | * Creates a radio button <input></input> element in the DOM. 756 | * The .option() method can be used to set options for the radio after it is 757 | * created. The .value() method will return the currently selected option. 758 | * 759 | * @method createRadio 760 | * @param {String} [divId] the id and name of the created div and input field respectively 761 | * @return {p5.Element} pointer to p5.Element holding created node 762 | * @example 763 | *
764 | * var radio; 765 | * 766 | * function setup() { 767 | * radio = createRadio(); 768 | * radio.option('black'); 769 | * radio.option('white'); 770 | * radio.option('gray'); 771 | * radio.style('width', '60px'); 772 | * textAlign(CENTER); 773 | * fill(255, 0, 0); 774 | * } 775 | * 776 | * function draw() { 777 | * var val = radio.value(); 778 | * background(val); 779 | * text(val, width / 2, height / 2); 780 | * } 781 | *
782 | *
783 | * var radio; 784 | * 785 | * function setup() { 786 | * radio = createRadio(); 787 | * radio.option('apple', 1); 788 | * radio.option('bread', 2); 789 | * radio.option('juice', 3); 790 | * radio.style('width', '60px'); 791 | * textAlign(CENTER); 792 | * } 793 | * 794 | * function draw() { 795 | * background(200); 796 | * var val = radio.value(); 797 | * if (val) { 798 | * text('item cost is $' + val, width / 2, height / 2); 799 | * } 800 | * } 801 | *
802 | */ 803 | p5.prototype.createRadio = function(existing_radios) { 804 | p5._validateParameters('createRadio', arguments); 805 | // do some prep by counting number of radios on page 806 | var radios = document.querySelectorAll('input[type=radio]'); 807 | var count = 0; 808 | if (radios.length > 1) { 809 | var length = radios.length; 810 | var prev = radios[0].name; 811 | var current = radios[1].name; 812 | count = 1; 813 | for (var i = 1; i < length; i++) { 814 | current = radios[i].name; 815 | if (prev !== current) { 816 | count++; 817 | } 818 | prev = current; 819 | } 820 | } else if (radios.length === 1) { 821 | count = 1; 822 | } 823 | // see if we got an existing set of radios from callee 824 | var elt, self; 825 | if (typeof existing_radios === 'object') { 826 | // use existing elements 827 | self = existing_radios; 828 | elt = this.elt = existing_radios.elt; 829 | } else { 830 | // create a set of radio buttons 831 | elt = document.createElement('div'); 832 | self = addElement(elt, this); 833 | } 834 | // setup member functions 835 | self._getInputChildrenArray = function() { 836 | return Array.prototype.slice.call(this.elt.children).filter(function(c) { 837 | return c.tagName === 'INPUT'; 838 | }); 839 | }; 840 | 841 | var times = -1; 842 | self.option = function(name, value) { 843 | var opt = document.createElement('input'); 844 | opt.type = 'radio'; 845 | opt.innerHTML = name; 846 | if (value) opt.value = value; 847 | else opt.value = name; 848 | opt.setAttribute('name', 'defaultradio' + count); 849 | elt.appendChild(opt); 850 | if (name) { 851 | times++; 852 | var label = document.createElement('label'); 853 | opt.setAttribute('id', 'defaultradio' + count + '-' + times); 854 | label.htmlFor = 'defaultradio' + count + '-' + times; 855 | label.appendChild(document.createTextNode(name)); 856 | elt.appendChild(label); 857 | } 858 | return opt; 859 | }; 860 | self.selected = function(value) { 861 | var i; 862 | var inputChildren = self._getInputChildrenArray(); 863 | if (value) { 864 | for (i = 0; i < inputChildren.length; i++) { 865 | if (inputChildren[i].value === value) inputChildren[i].checked = true; 866 | } 867 | return this; 868 | } else { 869 | for (i = 0; i < inputChildren.length; i++) { 870 | if (inputChildren[i].checked === true) return inputChildren[i].value; 871 | } 872 | } 873 | }; 874 | self.value = function(value) { 875 | var i; 876 | var inputChildren = self._getInputChildrenArray(); 877 | if (value) { 878 | for (i = 0; i < inputChildren.length; i++) { 879 | if (inputChildren[i].value === value) inputChildren[i].checked = true; 880 | } 881 | return this; 882 | } else { 883 | for (i = 0; i < inputChildren.length; i++) { 884 | if (inputChildren[i].checked === true) return inputChildren[i].value; 885 | } 886 | return ''; 887 | } 888 | }; 889 | return self; 890 | }; 891 | 892 | /** 893 | * Creates a colorPicker element in the DOM for color input. 894 | * The .value() method will return a hex string (#rrggbb) of the color. 895 | * The .color() method will return a p5.Color object with the current chosen color. 896 | * 897 | * @method createColorPicker 898 | * @param {String|p5.Color} [value] default color of element 899 | * @return {p5.Element} pointer to p5.Element holding created node 900 | * @example 901 | *
902 | * 903 | * var inp1, inp2; 904 | * function setup() { 905 | * createCanvas(100, 100); 906 | * background('grey'); 907 | * inp1 = createColorPicker('#ff0000'); 908 | * inp2 = createColorPicker(color('yellow')); 909 | * inp1.input(setShade1); 910 | * inp2.input(setShade2); 911 | * setMidShade(); 912 | * } 913 | * 914 | * function setMidShade() { 915 | * // Finding a shade between the two 916 | * var commonShade = lerpColor(inp1.color(), inp2.color(), 0.5); 917 | * fill(commonShade); 918 | * rect(20, 20, 60, 60); 919 | * } 920 | * 921 | * function setShade1() { 922 | * setMidShade(); 923 | * console.log('You are choosing shade 1 to be : ', this.value()); 924 | * } 925 | * function setShade2() { 926 | * setMidShade(); 927 | * console.log('You are choosing shade 2 to be : ', this.value()); 928 | * } 929 | * 930 | *
931 | */ 932 | p5.prototype.createColorPicker = function(value) { 933 | p5._validateParameters('createColorPicker', arguments); 934 | var elt = document.createElement('input'); 935 | var self; 936 | elt.type = 'color'; 937 | if (value) { 938 | if (value instanceof p5.Color) { 939 | elt.value = value.toString('#rrggbb'); 940 | } else { 941 | p5.prototype._colorMode = 'rgb'; 942 | p5.prototype._colorMaxes = { 943 | rgb: [255, 255, 255, 255], 944 | hsb: [360, 100, 100, 1], 945 | hsl: [360, 100, 100, 1] 946 | }; 947 | elt.value = p5.prototype.color(value).toString('#rrggbb'); 948 | } 949 | } else { 950 | elt.value = '#000000'; 951 | } 952 | self = addElement(elt, this); 953 | // Method to return a p5.Color object for the given color. 954 | self.color = function() { 955 | if (value.mode) { 956 | p5.prototype._colorMode = value.mode; 957 | } 958 | if (value.maxes) { 959 | p5.prototype._colorMaxes = value.maxes; 960 | } 961 | return p5.prototype.color(this.elt.value); 962 | }; 963 | return self; 964 | }; 965 | 966 | /** 967 | * Creates an <input></input> element in the DOM for text input. 968 | * Use .size() to set the display length of the box. 969 | * Appends to the container node if one is specified, otherwise 970 | * appends to body. 971 | * 972 | * @method createInput 973 | * @param {String} [value] default value of the input box 974 | * @param {String} [type] type of text, ie text, password etc. Defaults to text 975 | * @return {p5.Element} pointer to p5.Element holding created node 976 | * @example 977 | *
978 | * function setup() { 979 | * var inp = createInput(''); 980 | * inp.input(myInputEvent); 981 | * } 982 | * 983 | * function myInputEvent() { 984 | * console.log('you are typing: ', this.value()); 985 | * } 986 | *
987 | */ 988 | p5.prototype.createInput = function(value, type) { 989 | p5._validateParameters('createInput', arguments); 990 | var elt = document.createElement('input'); 991 | elt.type = type ? type : 'text'; 992 | if (value) elt.value = value; 993 | return addElement(elt, this); 994 | }; 995 | 996 | /** 997 | * Creates an <input></input> element in the DOM of type 'file'. 998 | * This allows users to select local files for use in a sketch. 999 | * 1000 | * @method createFileInput 1001 | * @param {Function} [callback] callback function for when a file loaded 1002 | * @param {String} [multiple] optional to allow multiple files selected 1003 | * @return {p5.Element} pointer to p5.Element holding created DOM element 1004 | * @example 1005 | *
1006 | * var input; 1007 | * var img; 1008 | * 1009 | * function setup() { 1010 | * input = createFileInput(handleFile); 1011 | * input.position(0, 0); 1012 | * } 1013 | * 1014 | * function draw() { 1015 | * if (img) { 1016 | * image(img, 0, 0, width, height); 1017 | * } 1018 | * } 1019 | * 1020 | * function handleFile(file) { 1021 | * print(file); 1022 | * if (file.type === 'image') { 1023 | * img = createImg(file.data); 1024 | * img.hide(); 1025 | * } 1026 | * } 1027 | *
1028 | */ 1029 | p5.prototype.createFileInput = function(callback, multiple) { 1030 | p5._validateParameters('createFileInput', arguments); 1031 | // Function to handle when a file is selected 1032 | // We're simplifying life and assuming that we always 1033 | // want to load every selected file 1034 | function handleFileSelect(evt) { 1035 | // These are the files 1036 | var files = evt.target.files; 1037 | // Load each one and trigger a callback 1038 | for (var i = 0; i < files.length; i++) { 1039 | var f = files[i]; 1040 | p5.File._load(f, callback); 1041 | } 1042 | } 1043 | // Is the file stuff supported? 1044 | if (window.File && window.FileReader && window.FileList && window.Blob) { 1045 | // Yup, we're ok and make an input file selector 1046 | var elt = document.createElement('input'); 1047 | elt.type = 'file'; 1048 | 1049 | // If we get a second argument that evaluates to true 1050 | // then we are looking for multiple files 1051 | if (multiple) { 1052 | // Anything gets the job done 1053 | elt.multiple = 'multiple'; 1054 | } 1055 | 1056 | // Now let's handle when a file was selected 1057 | elt.addEventListener('change', handleFileSelect, false); 1058 | return addElement(elt, this); 1059 | } else { 1060 | console.log( 1061 | 'The File APIs are not fully supported in this browser. Cannot create element.' 1062 | ); 1063 | } 1064 | }; 1065 | 1066 | /** VIDEO STUFF **/ 1067 | 1068 | function createMedia(pInst, type, src, callback) { 1069 | var elt = document.createElement(type); 1070 | 1071 | // allow src to be empty 1072 | src = src || ''; 1073 | if (typeof src === 'string') { 1074 | src = [src]; 1075 | } 1076 | for (var i = 0; i < src.length; i++) { 1077 | var source = document.createElement('source'); 1078 | source.src = src[i]; 1079 | elt.appendChild(source); 1080 | } 1081 | if (typeof callback !== 'undefined') { 1082 | var callbackHandler = function() { 1083 | callback(); 1084 | elt.removeEventListener('canplaythrough', callbackHandler); 1085 | }; 1086 | elt.addEventListener('canplaythrough', callbackHandler); 1087 | } 1088 | 1089 | var c = addElement(elt, pInst, true); 1090 | c.loadedmetadata = false; 1091 | // set width and height onload metadata 1092 | elt.addEventListener('loadedmetadata', function() { 1093 | c.width = elt.videoWidth; 1094 | c.height = elt.videoHeight; 1095 | //c.elt.playbackRate = s; 1096 | // set elt width and height if not set 1097 | if (c.elt.width === 0) c.elt.width = elt.videoWidth; 1098 | if (c.elt.height === 0) c.elt.height = elt.videoHeight; 1099 | if (c.presetPlaybackRate) { 1100 | c.elt.playbackRate = c.presetPlaybackRate; 1101 | delete c.presetPlaybackRate; 1102 | } 1103 | c.loadedmetadata = true; 1104 | }); 1105 | 1106 | return c; 1107 | } 1108 | /** 1109 | * Creates an HTML5 <video> element in the DOM for simple playback 1110 | * of audio/video. Shown by default, can be hidden with .hide() 1111 | * and drawn into canvas using video(). Appends to the container 1112 | * node if one is specified, otherwise appends to body. The first parameter 1113 | * can be either a single string path to a video file, or an array of string 1114 | * paths to different formats of the same video. This is useful for ensuring 1115 | * that your video can play across different browsers, as each supports 1116 | * different formats. See this 1117 | * page for further information about supported formats. 1118 | * 1119 | * @method createVideo 1120 | * @param {String|String[]} src path to a video file, or array of paths for 1121 | * supporting different browsers 1122 | * @param {Function} [callback] callback function to be called upon 1123 | * 'canplaythrough' event fire, that is, when the 1124 | * browser can play the media, and estimates that 1125 | * enough data has been loaded to play the media 1126 | * up to its end without having to stop for 1127 | * further buffering of content 1128 | * @return {p5.MediaElement} pointer to video p5.Element 1129 | * @example 1130 | *
1131 | * var vid; 1132 | * function setup() { 1133 | * vid = createVideo(['small.mp4', 'small.ogv', 'small.webm'], vidLoad); 1134 | * } 1135 | * 1136 | * // This function is called when the video loads 1137 | * function vidLoad() { 1138 | * vid.play(); 1139 | * } 1140 | *
1141 | */ 1142 | p5.prototype.createVideo = function(src, callback) { 1143 | p5._validateParameters('createVideo', arguments); 1144 | return createMedia(this, 'video', src, callback); 1145 | }; 1146 | 1147 | /** AUDIO STUFF **/ 1148 | 1149 | /** 1150 | * Creates a hidden HTML5 <audio> element in the DOM for simple audio 1151 | * playback. Appends to the container node if one is specified, 1152 | * otherwise appends to body. The first parameter 1153 | * can be either a single string path to a audio file, or an array of string 1154 | * paths to different formats of the same audio. This is useful for ensuring 1155 | * that your audio can play across different browsers, as each supports 1156 | * different formats. See this 1157 | * page for further information about supported formats. 1158 | * 1159 | * @method createAudio 1160 | * @param {String|String[]} [src] path to an audio file, or array of paths 1161 | * for supporting different browsers 1162 | * @param {Function} [callback] callback function to be called upon 1163 | * 'canplaythrough' event fire, that is, when the 1164 | * browser can play the media, and estimates that 1165 | * enough data has been loaded to play the media 1166 | * up to its end without having to stop for 1167 | * further buffering of content 1168 | * @return {p5.MediaElement} pointer to audio p5.Element 1169 | * @example 1170 | *
1171 | * var ele; 1172 | * function setup() { 1173 | * ele = createAudio('assets/beat.mp3'); 1174 | * 1175 | * // here we set the element to autoplay 1176 | * // The element will play as soon 1177 | * // as it is able to do so. 1178 | * ele.autoplay(true); 1179 | * } 1180 | *
1181 | */ 1182 | p5.prototype.createAudio = function(src, callback) { 1183 | p5._validateParameters('createAudio', arguments); 1184 | return createMedia(this, 'audio', src, callback); 1185 | }; 1186 | 1187 | /** CAMERA STUFF **/ 1188 | 1189 | /** 1190 | * @property {String} VIDEO 1191 | * @final 1192 | * @category Constants 1193 | */ 1194 | p5.prototype.VIDEO = 'video'; 1195 | /** 1196 | * @property {String} AUDIO 1197 | * @final 1198 | * @category Constants 1199 | */ 1200 | p5.prototype.AUDIO = 'audio'; 1201 | 1202 | // from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia 1203 | // Older browsers might not implement mediaDevices at all, so we set an empty object first 1204 | if (navigator.mediaDevices === undefined) { 1205 | navigator.mediaDevices = {}; 1206 | } 1207 | 1208 | // Some browsers partially implement mediaDevices. We can't just assign an object 1209 | // with getUserMedia as it would overwrite existing properties. 1210 | // Here, we will just add the getUserMedia property if it's missing. 1211 | if (navigator.mediaDevices.getUserMedia === undefined) { 1212 | navigator.mediaDevices.getUserMedia = function(constraints) { 1213 | // First get ahold of the legacy getUserMedia, if present 1214 | var getUserMedia = 1215 | navigator.webkitGetUserMedia || navigator.mozGetUserMedia; 1216 | 1217 | // Some browsers just don't implement it - return a rejected promise with an error 1218 | // to keep a consistent interface 1219 | if (!getUserMedia) { 1220 | return Promise.reject( 1221 | new Error('getUserMedia is not implemented in this browser') 1222 | ); 1223 | } 1224 | 1225 | // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise 1226 | return new Promise(function(resolve, reject) { 1227 | getUserMedia.call(navigator, constraints, resolve, reject); 1228 | }); 1229 | }; 1230 | } 1231 | 1232 | /** 1233 | *

Creates a new HTML5 <video> element that contains the audio/video 1234 | * feed from a webcam. The element is separate from the canvas and is 1235 | * displayed by default. The element can be hidden using .hide(). The feed 1236 | * can be drawn onto the canvas using image(). The loadedmetadata property can 1237 | * be used to detect when the element has fully loaded (see second example).

1238 | *

More specific properties of the feed can be passing in a Constraints object. 1239 | * See the 1240 | * W3C 1241 | * spec for possible properties. Note that not all of these are supported 1242 | * by all browsers.

1243 | *

Security note: A new browser security specification requires that getUserMedia, 1244 | * which is behind createCapture(), only works when you're running the code locally, 1245 | * or on HTTPS. Learn more here 1246 | * and here.

1247 | * 1248 | * @method createCapture 1249 | * @param {String|Constant|Object} type type of capture, either VIDEO or 1250 | * AUDIO if none specified, default both, 1251 | * or a Constraints object 1252 | * @param {Function} [callback] function to be called once 1253 | * stream has loaded 1254 | * @return {p5.Element} capture video p5.Element 1255 | * @example 1256 | *
1257 | * var capture; 1258 | * 1259 | * function setup() { 1260 | * createCanvas(480, 480); 1261 | * capture = createCapture(VIDEO); 1262 | * capture.hide(); 1263 | * } 1264 | * 1265 | * function draw() { 1266 | * image(capture, 0, 0, width, width * capture.height / capture.width); 1267 | * filter(INVERT); 1268 | * } 1269 | *
1270 | *
1271 | * function setup() { 1272 | * createCanvas(480, 120); 1273 | * var constraints = { 1274 | * video: { 1275 | * mandatory: { 1276 | * minWidth: 1280, 1277 | * minHeight: 720 1278 | * }, 1279 | * optional: [{ maxFrameRate: 10 }] 1280 | * }, 1281 | * audio: true 1282 | * }; 1283 | * createCapture(constraints, function(stream) { 1284 | * console.log(stream); 1285 | * }); 1286 | * } 1287 | *
1288 | *
1289 | * var capture; 1290 | * 1291 | * function setup() { 1292 | * createCanvas(640, 480); 1293 | * capture = createCapture(VIDEO); 1294 | * } 1295 | * function draw() { 1296 | * background(0); 1297 | * if (capture.loadedmetadata) { 1298 | * var c = capture.get(0, 0, 100, 100); 1299 | * image(c, 0, 0); 1300 | * } 1301 | * } 1302 | */ 1303 | p5.prototype.createCapture = function() { 1304 | p5._validateParameters('createCapture', arguments); 1305 | var useVideo = true; 1306 | var useAudio = true; 1307 | var constraints; 1308 | var cb; 1309 | for (var i = 0; i < arguments.length; i++) { 1310 | if (arguments[i] === p5.prototype.VIDEO) { 1311 | useAudio = false; 1312 | } else if (arguments[i] === p5.prototype.AUDIO) { 1313 | useVideo = false; 1314 | } else if (typeof arguments[i] === 'object') { 1315 | constraints = arguments[i]; 1316 | } else if (typeof arguments[i] === 'function') { 1317 | cb = arguments[i]; 1318 | } 1319 | } 1320 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 1321 | var elt = document.createElement('video'); 1322 | // required to work in iOS 11 & up: 1323 | elt.setAttribute('playsinline', ''); 1324 | 1325 | if (!constraints) { 1326 | constraints = { video: useVideo, audio: useAudio }; 1327 | } 1328 | 1329 | navigator.mediaDevices.getUserMedia(constraints).then( 1330 | function(stream) { 1331 | try { 1332 | if ('srcObject' in elt) { 1333 | elt.srcObject = stream; 1334 | } else { 1335 | elt.src = window.URL.createObjectURL(stream); 1336 | } 1337 | } catch (err) { 1338 | elt.src = stream; 1339 | } 1340 | if (cb) { 1341 | cb(stream); 1342 | } 1343 | }, 1344 | function(e) { 1345 | console.log(e); 1346 | } 1347 | ); 1348 | } else { 1349 | throw 'getUserMedia not supported in this browser'; 1350 | } 1351 | var c = addElement(elt, this, true); 1352 | c.loadedmetadata = false; 1353 | // set width and height onload metadata 1354 | elt.addEventListener('loadedmetadata', function() { 1355 | elt.play(); 1356 | if (elt.width) { 1357 | c.width = elt.videoWidth = elt.width; 1358 | c.height = elt.videoHeight = elt.height; 1359 | } else { 1360 | c.width = c.elt.width = elt.videoWidth; 1361 | c.height = c.elt.height = elt.videoHeight; 1362 | } 1363 | c.loadedmetadata = true; 1364 | }); 1365 | return c; 1366 | }; 1367 | 1368 | /** 1369 | * Creates element with given tag in the DOM with given content. 1370 | * Appends to the container node if one is specified, otherwise 1371 | * appends to body. 1372 | * 1373 | * @method createElement 1374 | * @param {String} tag tag for the new element 1375 | * @param {String} [content] html content to be inserted into the element 1376 | * @return {p5.Element} pointer to p5.Element holding created node 1377 | * @example 1378 | *
1379 | * createElement('h2', 'im an h2 p5.element!'); 1380 | *
1381 | */ 1382 | p5.prototype.createElement = function(tag, content) { 1383 | p5._validateParameters('createElement', arguments); 1384 | var elt = document.createElement(tag); 1385 | if (typeof content !== 'undefined') { 1386 | elt.innerHTML = content; 1387 | } 1388 | return addElement(elt, this); 1389 | }; 1390 | 1391 | // ============================================================================= 1392 | // p5.Element additions 1393 | // ============================================================================= 1394 | /** 1395 | * 1396 | * Adds specified class to the element. 1397 | * 1398 | * @for p5.Element 1399 | * @method addClass 1400 | * @param {String} class name of class to add 1401 | * @chainable 1402 | * @example 1403 | *
1404 | * var div = createDiv('div'); 1405 | * div.addClass('myClass'); 1406 | *
1407 | */ 1408 | p5.Element.prototype.addClass = function(c) { 1409 | if (this.elt.className) { 1410 | // PEND don't add class more than once 1411 | //var regex = new RegExp('[^a-zA-Z\d:]?'+c+'[^a-zA-Z\d:]?'); 1412 | //if (this.elt.className.search(/[^a-zA-Z\d:]?hi[^a-zA-Z\d:]?/) === -1) { 1413 | this.elt.className = this.elt.className + ' ' + c; 1414 | //} 1415 | } else { 1416 | this.elt.className = c; 1417 | } 1418 | return this; 1419 | }; 1420 | 1421 | /** 1422 | * 1423 | * Removes specified class from the element. 1424 | * 1425 | * @method removeClass 1426 | * @param {String} class name of class to remove 1427 | * @chainable 1428 | * @example 1429 | *
1430 | * // In this example, a class is set when the div is created 1431 | * // and removed when mouse is pressed. This could link up 1432 | * // with a CSS style rule to toggle style properties. 1433 | * 1434 | * var div; 1435 | * 1436 | * function setup() { 1437 | * div = createDiv('div'); 1438 | * div.addClass('myClass'); 1439 | * } 1440 | * 1441 | * function mousePressed() { 1442 | * div.removeClass('myClass'); 1443 | * } 1444 | *
1445 | */ 1446 | p5.Element.prototype.removeClass = function(c) { 1447 | var regex = new RegExp('(?:^|\\s)' + c + '(?!\\S)'); 1448 | this.elt.className = this.elt.className.replace(regex, ''); 1449 | this.elt.className = this.elt.className.replace(/^\s+|\s+$/g, ''); //prettify (optional) 1450 | return this; 1451 | }; 1452 | 1453 | /** 1454 | * 1455 | * Attaches the element as a child to the parent specified. 1456 | * Accepts either a string ID, DOM node, or p5.Element. 1457 | * If no argument is specified, an array of children DOM nodes is returned. 1458 | * 1459 | * @method child 1460 | * @returns {Node[]} an array of child nodes 1461 | * @example 1462 | *
1463 | * var div0 = createDiv('this is the parent'); 1464 | * var div1 = createDiv('this is the child'); 1465 | * div0.child(div1); // use p5.Element 1466 | *
1467 | *
1468 | * var div0 = createDiv('this is the parent'); 1469 | * var div1 = createDiv('this is the child'); 1470 | * div1.id('apples'); 1471 | * div0.child('apples'); // use id 1472 | *
1473 | *
1474 | * // this example assumes there is a div already on the page 1475 | * // with id "myChildDiv" 1476 | * var div0 = createDiv('this is the parent'); 1477 | * var elt = document.getElementById('myChildDiv'); 1478 | * div0.child(elt); // use element from page 1479 | *
1480 | */ 1481 | /** 1482 | * @method child 1483 | * @param {String|p5.Element} [child] the ID, DOM node, or p5.Element 1484 | * to add to the current element 1485 | * @chainable 1486 | */ 1487 | p5.Element.prototype.child = function(c) { 1488 | if (typeof c === 'undefined') { 1489 | return this.elt.childNodes; 1490 | } 1491 | if (typeof c === 'string') { 1492 | if (c[0] === '#') { 1493 | c = c.substring(1); 1494 | } 1495 | c = document.getElementById(c); 1496 | } else if (c instanceof p5.Element) { 1497 | c = c.elt; 1498 | } 1499 | this.elt.appendChild(c); 1500 | return this; 1501 | }; 1502 | 1503 | /** 1504 | * Centers a p5 Element either vertically, horizontally, 1505 | * or both, relative to its parent or according to 1506 | * the body if the Element has no parent. If no argument is passed 1507 | * the Element is aligned both vertically and horizontally. 1508 | * 1509 | * @method center 1510 | * @param {String} [align] passing 'vertical', 'horizontal' aligns element accordingly 1511 | * @chainable 1512 | * 1513 | * @example 1514 | *
1515 | * function setup() { 1516 | * var div = createDiv('').size(10, 10); 1517 | * div.style('background-color', 'orange'); 1518 | * div.center(); 1519 | * } 1520 | *
1521 | */ 1522 | p5.Element.prototype.center = function(align) { 1523 | var style = this.elt.style.display; 1524 | var hidden = this.elt.style.display === 'none'; 1525 | var parentHidden = this.parent().style.display === 'none'; 1526 | var pos = { x: this.elt.offsetLeft, y: this.elt.offsetTop }; 1527 | 1528 | if (hidden) this.show(); 1529 | 1530 | this.elt.style.display = 'block'; 1531 | this.position(0, 0); 1532 | 1533 | if (parentHidden) this.parent().style.display = 'block'; 1534 | 1535 | var wOffset = Math.abs(this.parent().offsetWidth - this.elt.offsetWidth); 1536 | var hOffset = Math.abs(this.parent().offsetHeight - this.elt.offsetHeight); 1537 | var y = pos.y; 1538 | var x = pos.x; 1539 | 1540 | if (align === 'both' || align === undefined) { 1541 | this.position(wOffset / 2, hOffset / 2); 1542 | } else if (align === 'horizontal') { 1543 | this.position(wOffset / 2, y); 1544 | } else if (align === 'vertical') { 1545 | this.position(x, hOffset / 2); 1546 | } 1547 | 1548 | this.style('display', style); 1549 | 1550 | if (hidden) this.hide(); 1551 | 1552 | if (parentHidden) this.parent().style.display = 'none'; 1553 | 1554 | return this; 1555 | }; 1556 | 1557 | /** 1558 | * 1559 | * If an argument is given, sets the inner HTML of the element, 1560 | * replacing any existing html. If true is included as a second 1561 | * argument, html is appended instead of replacing existing html. 1562 | * If no arguments are given, returns 1563 | * the inner HTML of the element. 1564 | * 1565 | * @for p5.Element 1566 | * @method html 1567 | * @returns {String} the inner HTML of the element 1568 | * @example 1569 | *
1570 | * var div = createDiv('').size(100, 100); 1571 | * div.html('hi'); 1572 | *
1573 | *
1574 | * var div = createDiv('Hello ').size(100, 100); 1575 | * div.html('World', true); 1576 | *
1577 | */ 1578 | /** 1579 | * @method html 1580 | * @param {String} [html] the HTML to be placed inside the element 1581 | * @param {boolean} [append] whether to append HTML to existing 1582 | * @chainable 1583 | */ 1584 | p5.Element.prototype.html = function() { 1585 | if (arguments.length === 0) { 1586 | return this.elt.innerHTML; 1587 | } else if (arguments[1]) { 1588 | this.elt.innerHTML += arguments[0]; 1589 | return this; 1590 | } else { 1591 | this.elt.innerHTML = arguments[0]; 1592 | return this; 1593 | } 1594 | }; 1595 | 1596 | /** 1597 | * 1598 | * Sets the position of the element relative to (0, 0) of the 1599 | * window. Essentially, sets position:absolute and left and top 1600 | * properties of style. If no arguments given returns the x and y position 1601 | * of the element in an object. 1602 | * 1603 | * @method position 1604 | * @returns {Object} the x and y position of the element in an object 1605 | * @example 1606 | *
1607 | * function setup() { 1608 | * var cnv = createCanvas(100, 100); 1609 | * // positions canvas 50px to the right and 100px 1610 | * // below upper left corner of the window 1611 | * cnv.position(50, 100); 1612 | * } 1613 | *
1614 | */ 1615 | /** 1616 | * @method position 1617 | * @param {Number} [x] x-position relative to upper left of window 1618 | * @param {Number} [y] y-position relative to upper left of window 1619 | * @chainable 1620 | */ 1621 | p5.Element.prototype.position = function() { 1622 | if (arguments.length === 0) { 1623 | return { x: this.elt.offsetLeft, y: this.elt.offsetTop }; 1624 | } else { 1625 | this.elt.style.position = 'absolute'; 1626 | this.elt.style.left = arguments[0] + 'px'; 1627 | this.elt.style.top = arguments[1] + 'px'; 1628 | this.x = arguments[0]; 1629 | this.y = arguments[1]; 1630 | return this; 1631 | } 1632 | }; 1633 | 1634 | /* Helper method called by p5.Element.style() */ 1635 | p5.Element.prototype._translate = function() { 1636 | this.elt.style.position = 'absolute'; 1637 | // save out initial non-translate transform styling 1638 | var transform = ''; 1639 | if (this.elt.style.transform) { 1640 | transform = this.elt.style.transform.replace(/translate3d\(.*\)/g, ''); 1641 | transform = transform.replace(/translate[X-Z]?\(.*\)/g, ''); 1642 | } 1643 | if (arguments.length === 2) { 1644 | this.elt.style.transform = 1645 | 'translate(' + arguments[0] + 'px, ' + arguments[1] + 'px)'; 1646 | } else if (arguments.length > 2) { 1647 | this.elt.style.transform = 1648 | 'translate3d(' + 1649 | arguments[0] + 1650 | 'px,' + 1651 | arguments[1] + 1652 | 'px,' + 1653 | arguments[2] + 1654 | 'px)'; 1655 | if (arguments.length === 3) { 1656 | this.elt.parentElement.style.perspective = '1000px'; 1657 | } else { 1658 | this.elt.parentElement.style.perspective = arguments[3] + 'px'; 1659 | } 1660 | } 1661 | // add any extra transform styling back on end 1662 | this.elt.style.transform += transform; 1663 | return this; 1664 | }; 1665 | 1666 | /* Helper method called by p5.Element.style() */ 1667 | p5.Element.prototype._rotate = function() { 1668 | // save out initial non-rotate transform styling 1669 | var transform = ''; 1670 | if (this.elt.style.transform) { 1671 | transform = this.elt.style.transform.replace(/rotate3d\(.*\)/g, ''); 1672 | transform = transform.replace(/rotate[X-Z]?\(.*\)/g, ''); 1673 | } 1674 | 1675 | if (arguments.length === 1) { 1676 | this.elt.style.transform = 'rotate(' + arguments[0] + 'deg)'; 1677 | } else if (arguments.length === 2) { 1678 | this.elt.style.transform = 1679 | 'rotate(' + arguments[0] + 'deg, ' + arguments[1] + 'deg)'; 1680 | } else if (arguments.length === 3) { 1681 | this.elt.style.transform = 'rotateX(' + arguments[0] + 'deg)'; 1682 | this.elt.style.transform += 'rotateY(' + arguments[1] + 'deg)'; 1683 | this.elt.style.transform += 'rotateZ(' + arguments[2] + 'deg)'; 1684 | } 1685 | // add remaining transform back on 1686 | this.elt.style.transform += transform; 1687 | return this; 1688 | }; 1689 | 1690 | /** 1691 | * Sets the given style (css) property (1st arg) of the element with the 1692 | * given value (2nd arg). If a single argument is given, .style() 1693 | * returns the value of the given property; however, if the single argument 1694 | * is given in css syntax ('text-align:center'), .style() sets the css 1695 | * appropriatly. .style() also handles 2d and 3d css transforms. If 1696 | * the 1st arg is 'rotate', 'translate', or 'position', the following arguments 1697 | * accept Numbers as values. ('translate', 10, 100, 50); 1698 | * 1699 | * @method style 1700 | * @param {String} property property to be set 1701 | * @returns {String} value of property 1702 | * @example 1703 | *
1704 | * var myDiv = createDiv('I like pandas.'); 1705 | * myDiv.style('font-size', '18px'); 1706 | * myDiv.style('color', '#ff0000'); 1707 | *
1708 | *
1709 | * var col = color(25, 23, 200, 50); 1710 | * var button = createButton('button'); 1711 | * button.style('background-color', col); 1712 | * button.position(10, 10); 1713 | *
1714 | *
1715 | * var myDiv = createDiv('I like lizards.'); 1716 | * myDiv.style('position', 20, 20); 1717 | * myDiv.style('rotate', 45); 1718 | *
1719 | *
1720 | * var myDiv; 1721 | * function setup() { 1722 | * background(200); 1723 | * myDiv = createDiv('I like gray.'); 1724 | * myDiv.position(20, 20); 1725 | * } 1726 | * 1727 | * function draw() { 1728 | * myDiv.style('font-size', mouseX + 'px'); 1729 | * } 1730 | *
1731 | */ 1732 | /** 1733 | * @method style 1734 | * @param {String} property 1735 | * @param {String|Number|p5.Color} value value to assign to property (only String|Number for rotate/translate) 1736 | * @param {String|Number|p5.Color} [value2] position can take a 2nd value 1737 | * @param {String|Number|p5.Color} [value3] translate can take a 2nd & 3rd value 1738 | * @chainable 1739 | */ 1740 | p5.Element.prototype.style = function(prop, val) { 1741 | var self = this; 1742 | 1743 | if (val instanceof p5.Color) { 1744 | val = 1745 | 'rgba(' + 1746 | val.levels[0] + 1747 | ',' + 1748 | val.levels[1] + 1749 | ',' + 1750 | val.levels[2] + 1751 | ',' + 1752 | val.levels[3] / 255 + 1753 | ')'; 1754 | } 1755 | 1756 | if (typeof val === 'undefined') { 1757 | if (prop.indexOf(':') === -1) { 1758 | var styles = window.getComputedStyle(self.elt); 1759 | var style = styles.getPropertyValue(prop); 1760 | return style; 1761 | } else { 1762 | var attrs = prop.split(';'); 1763 | for (var i = 0; i < attrs.length; i++) { 1764 | var parts = attrs[i].split(':'); 1765 | if (parts[0] && parts[1]) { 1766 | this.elt.style[parts[0].trim()] = parts[1].trim(); 1767 | } 1768 | } 1769 | } 1770 | } else { 1771 | if (prop === 'rotate' || prop === 'translate' || prop === 'position') { 1772 | var trans = Array.prototype.shift.apply(arguments); 1773 | var f = this[trans] || this['_' + trans]; 1774 | f.apply(this, arguments); 1775 | } else { 1776 | this.elt.style[prop] = val; 1777 | if ( 1778 | prop === 'width' || 1779 | prop === 'height' || 1780 | prop === 'left' || 1781 | prop === 'top' 1782 | ) { 1783 | var numVal = val.replace(/\D+/g, ''); 1784 | this[prop] = parseInt(numVal, 10); // pend: is this necessary? 1785 | } 1786 | } 1787 | } 1788 | return this; 1789 | }; 1790 | 1791 | /** 1792 | * 1793 | * Adds a new attribute or changes the value of an existing attribute 1794 | * on the specified element. If no value is specified, returns the 1795 | * value of the given attribute, or null if attribute is not set. 1796 | * 1797 | * @method attribute 1798 | * @return {String} value of attribute 1799 | * 1800 | * @example 1801 | *
1802 | * var myDiv = createDiv('I like pandas.'); 1803 | * myDiv.attribute('align', 'center'); 1804 | *
1805 | */ 1806 | /** 1807 | * @method attribute 1808 | * @param {String} attr attribute to set 1809 | * @param {String} value value to assign to attribute 1810 | * @chainable 1811 | */ 1812 | p5.Element.prototype.attribute = function(attr, value) { 1813 | //handling for checkboxes and radios to ensure options get 1814 | //attributes not divs 1815 | if ( 1816 | this.elt.firstChild != null && 1817 | (this.elt.firstChild.type === 'checkbox' || 1818 | this.elt.firstChild.type === 'radio') 1819 | ) { 1820 | if (typeof value === 'undefined') { 1821 | return this.elt.firstChild.getAttribute(attr); 1822 | } else { 1823 | for (var i = 0; i < this.elt.childNodes.length; i++) { 1824 | this.elt.childNodes[i].setAttribute(attr, value); 1825 | } 1826 | } 1827 | } else if (typeof value === 'undefined') { 1828 | return this.elt.getAttribute(attr); 1829 | } else { 1830 | this.elt.setAttribute(attr, value); 1831 | return this; 1832 | } 1833 | }; 1834 | 1835 | /** 1836 | * 1837 | * Removes an attribute on the specified element. 1838 | * 1839 | * @method removeAttribute 1840 | * @param {String} attr attribute to remove 1841 | * @chainable 1842 | * 1843 | * @example 1844 | *
1845 | * var button; 1846 | * var checkbox; 1847 | * 1848 | * function setup() { 1849 | * checkbox = createCheckbox('enable', true); 1850 | * checkbox.changed(enableButton); 1851 | * button = createButton('button'); 1852 | * button.position(10, 10); 1853 | * } 1854 | * 1855 | * function enableButton() { 1856 | * if (this.checked()) { 1857 | * // Re-enable the button 1858 | * button.removeAttribute('disabled'); 1859 | * } else { 1860 | * // Disable the button 1861 | * button.attribute('disabled', ''); 1862 | * } 1863 | * } 1864 | *
1865 | */ 1866 | p5.Element.prototype.removeAttribute = function(attr) { 1867 | if ( 1868 | this.elt.firstChild != null && 1869 | (this.elt.firstChild.type === 'checkbox' || 1870 | this.elt.firstChild.type === 'radio') 1871 | ) { 1872 | for (var i = 0; i < this.elt.childNodes.length; i++) { 1873 | this.elt.childNodes[i].removeAttribute(attr); 1874 | } 1875 | } 1876 | this.elt.removeAttribute(attr); 1877 | return this; 1878 | }; 1879 | 1880 | /** 1881 | * Either returns the value of the element if no arguments 1882 | * given, or sets the value of the element. 1883 | * 1884 | * @method value 1885 | * @return {String|Number} value of the element 1886 | * @example 1887 | *
1888 | * // gets the value 1889 | * var inp; 1890 | * function setup() { 1891 | * inp = createInput(''); 1892 | * } 1893 | * 1894 | * function mousePressed() { 1895 | * print(inp.value()); 1896 | * } 1897 | *
1898 | *
1899 | * // sets the value 1900 | * var inp; 1901 | * function setup() { 1902 | * inp = createInput('myValue'); 1903 | * } 1904 | * 1905 | * function mousePressed() { 1906 | * inp.value('myValue'); 1907 | * } 1908 | *
1909 | */ 1910 | /** 1911 | * @method value 1912 | * @param {String|Number} value 1913 | * @chainable 1914 | */ 1915 | p5.Element.prototype.value = function() { 1916 | if (arguments.length > 0) { 1917 | this.elt.value = arguments[0]; 1918 | return this; 1919 | } else { 1920 | if (this.elt.type === 'range') { 1921 | return parseFloat(this.elt.value); 1922 | } else return this.elt.value; 1923 | } 1924 | }; 1925 | 1926 | /** 1927 | * 1928 | * Shows the current element. Essentially, setting display:block for the style. 1929 | * 1930 | * @method show 1931 | * @chainable 1932 | * @example 1933 | *
1934 | * var div = createDiv('div'); 1935 | * div.style('display', 'none'); 1936 | * div.show(); // turns display to block 1937 | *
1938 | */ 1939 | p5.Element.prototype.show = function() { 1940 | this.elt.style.display = 'block'; 1941 | return this; 1942 | }; 1943 | 1944 | /** 1945 | * Hides the current element. Essentially, setting display:none for the style. 1946 | * 1947 | * @method hide 1948 | * @chainable 1949 | * @example 1950 | *
1951 | * var div = createDiv('this is a div'); 1952 | * div.hide(); 1953 | *
1954 | */ 1955 | p5.Element.prototype.hide = function() { 1956 | this.elt.style.display = 'none'; 1957 | return this; 1958 | }; 1959 | 1960 | /** 1961 | * 1962 | * Sets the width and height of the element. AUTO can be used to 1963 | * only adjust one dimension. If no arguments given returns the width and height 1964 | * of the element in an object. 1965 | * 1966 | * @method size 1967 | * @return {Object} the width and height of the element in an object 1968 | * @example 1969 | *
1970 | * var div = createDiv('this is a div'); 1971 | * div.size(100, 100); 1972 | *
1973 | */ 1974 | /** 1975 | * @method size 1976 | * @param {Number|Constant} w width of the element, either AUTO, or a number 1977 | * @param {Number|Constant} [h] height of the element, either AUTO, or a number 1978 | * @chainable 1979 | */ 1980 | p5.Element.prototype.size = function(w, h) { 1981 | if (arguments.length === 0) { 1982 | return { width: this.elt.offsetWidth, height: this.elt.offsetHeight }; 1983 | } else { 1984 | var aW = w; 1985 | var aH = h; 1986 | var AUTO = p5.prototype.AUTO; 1987 | if (aW !== AUTO || aH !== AUTO) { 1988 | if (aW === AUTO) { 1989 | aW = h * this.width / this.height; 1990 | } else if (aH === AUTO) { 1991 | aH = w * this.height / this.width; 1992 | } 1993 | // set diff for cnv vs normal div 1994 | if (this.elt instanceof HTMLCanvasElement) { 1995 | var j = {}; 1996 | var k = this.elt.getContext('2d'); 1997 | var prop; 1998 | for (prop in k) { 1999 | j[prop] = k[prop]; 2000 | } 2001 | this.elt.setAttribute('width', aW * this._pInst._pixelDensity); 2002 | this.elt.setAttribute('height', aH * this._pInst._pixelDensity); 2003 | this.elt.setAttribute( 2004 | 'style', 2005 | 'width:' + aW + 'px; height:' + aH + 'px' 2006 | ); 2007 | this._pInst.scale( 2008 | this._pInst._pixelDensity, 2009 | this._pInst._pixelDensity 2010 | ); 2011 | for (prop in j) { 2012 | this.elt.getContext('2d')[prop] = j[prop]; 2013 | } 2014 | } else { 2015 | this.elt.style.width = aW + 'px'; 2016 | this.elt.style.height = aH + 'px'; 2017 | this.elt.width = aW; 2018 | this.elt.height = aH; 2019 | this.width = aW; 2020 | this.height = aH; 2021 | } 2022 | 2023 | this.width = this.elt.offsetWidth; 2024 | this.height = this.elt.offsetHeight; 2025 | 2026 | if (this._pInst && this._pInst._curElement) { 2027 | // main canvas associated with p5 instance 2028 | if (this._pInst._curElement.elt === this.elt) { 2029 | this._pInst._setProperty('width', this.elt.offsetWidth); 2030 | this._pInst._setProperty('height', this.elt.offsetHeight); 2031 | } 2032 | } 2033 | } 2034 | return this; 2035 | } 2036 | }; 2037 | 2038 | /** 2039 | * Removes the element and deregisters all listeners. 2040 | * @method remove 2041 | * @example 2042 | *
2043 | * var myDiv = createDiv('this is some text'); 2044 | * myDiv.remove(); 2045 | *
2046 | */ 2047 | p5.Element.prototype.remove = function() { 2048 | // deregister events 2049 | for (var ev in this._events) { 2050 | this.elt.removeEventListener(ev, this._events[ev]); 2051 | } 2052 | if (this.elt.parentNode) { 2053 | this.elt.parentNode.removeChild(this.elt); 2054 | } 2055 | delete this; 2056 | }; 2057 | 2058 | // ============================================================================= 2059 | // p5.MediaElement additions 2060 | // ============================================================================= 2061 | 2062 | /** 2063 | * Extends p5.Element to handle audio and video. In addition to the methods 2064 | * of p5.Element, it also contains methods for controlling media. It is not 2065 | * called directly, but p5.MediaElements are created by calling createVideo, 2066 | * createAudio, and createCapture. 2067 | * 2068 | * @class p5.MediaElement 2069 | * @constructor 2070 | * @param {String} elt DOM node that is wrapped 2071 | */ 2072 | p5.MediaElement = function(elt, pInst) { 2073 | p5.Element.call(this, elt, pInst); 2074 | 2075 | var self = this; 2076 | this.elt.crossOrigin = 'anonymous'; 2077 | 2078 | this._prevTime = 0; 2079 | this._cueIDCounter = 0; 2080 | this._cues = []; 2081 | this._pixelDensity = 1; 2082 | this._modified = false; 2083 | this._pixelsDirty = true; 2084 | this._pixelsTime = -1; // the time at which we last updated 'pixels' 2085 | 2086 | /** 2087 | * Path to the media element source. 2088 | * 2089 | * @property src 2090 | * @return {String} src 2091 | * @example 2092 | *
2093 | * var ele; 2094 | * 2095 | * function setup() { 2096 | * background(250); 2097 | * 2098 | * //p5.MediaElement objects are usually created 2099 | * //by calling the createAudio(), createVideo(), 2100 | * //and createCapture() functions. 2101 | * 2102 | * //In this example we create 2103 | * //a new p5.MediaElement via createAudio(). 2104 | * ele = createAudio('assets/beat.mp3'); 2105 | * 2106 | * //We'll set up our example so that 2107 | * //when you click on the text, 2108 | * //an alert box displays the MediaElement's 2109 | * //src field. 2110 | * textAlign(CENTER); 2111 | * text('Click Me!', width / 2, height / 2); 2112 | * } 2113 | * 2114 | * function mouseClicked() { 2115 | * //here we test if the mouse is over the 2116 | * //canvas element when it's clicked 2117 | * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { 2118 | * //Show our p5.MediaElement's src field 2119 | * alert(ele.src); 2120 | * } 2121 | * } 2122 | *
2123 | */ 2124 | Object.defineProperty(self, 'src', { 2125 | get: function() { 2126 | var firstChildSrc = self.elt.children[0].src; 2127 | var srcVal = self.elt.src === window.location.href ? '' : self.elt.src; 2128 | var ret = 2129 | firstChildSrc === window.location.href ? srcVal : firstChildSrc; 2130 | return ret; 2131 | }, 2132 | set: function(newValue) { 2133 | for (var i = 0; i < self.elt.children.length; i++) { 2134 | self.elt.removeChild(self.elt.children[i]); 2135 | } 2136 | var source = document.createElement('source'); 2137 | source.src = newValue; 2138 | elt.appendChild(source); 2139 | self.elt.src = newValue; 2140 | self.modified = true; 2141 | } 2142 | }); 2143 | 2144 | // private _onended callback, set by the method: onended(callback) 2145 | self._onended = function() {}; 2146 | self.elt.onended = function() { 2147 | self._onended(self); 2148 | }; 2149 | }; 2150 | p5.MediaElement.prototype = Object.create(p5.Element.prototype); 2151 | 2152 | /** 2153 | * Play an HTML5 media element. 2154 | * 2155 | * @method play 2156 | * @chainable 2157 | * @example 2158 | *
2159 | * var ele; 2160 | * 2161 | * function setup() { 2162 | * //p5.MediaElement objects are usually created 2163 | * //by calling the createAudio(), createVideo(), 2164 | * //and createCapture() functions. 2165 | * 2166 | * //In this example we create 2167 | * //a new p5.MediaElement via createAudio(). 2168 | * ele = createAudio('assets/beat.mp3'); 2169 | * 2170 | * background(250); 2171 | * textAlign(CENTER); 2172 | * text('Click to Play!', width / 2, height / 2); 2173 | * } 2174 | * 2175 | * function mouseClicked() { 2176 | * //here we test if the mouse is over the 2177 | * //canvas element when it's clicked 2178 | * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { 2179 | * //Here we call the play() function on 2180 | * //the p5.MediaElement we created above. 2181 | * //This will start the audio sample. 2182 | * ele.play(); 2183 | * 2184 | * background(200); 2185 | * text('You clicked Play!', width / 2, height / 2); 2186 | * } 2187 | * } 2188 | *
2189 | */ 2190 | p5.MediaElement.prototype.play = function() { 2191 | if (this.elt.currentTime === this.elt.duration) { 2192 | this.elt.currentTime = 0; 2193 | } 2194 | var promise; 2195 | if (this.elt.readyState > 1) { 2196 | promise = this.elt.play(); 2197 | } else { 2198 | // in Chrome, playback cannot resume after being stopped and must reload 2199 | this.elt.load(); 2200 | promise = this.elt.play(); 2201 | } 2202 | if (promise && promise.catch) { 2203 | promise.catch(function(e) { 2204 | console.log( 2205 | 'WARN: Element play method raised an error asynchronously', 2206 | e 2207 | ); 2208 | }); 2209 | } 2210 | return this; 2211 | }; 2212 | 2213 | /** 2214 | * Stops an HTML5 media element (sets current time to zero). 2215 | * 2216 | * @method stop 2217 | * @chainable 2218 | * @example 2219 | *
2220 | * //This example both starts 2221 | * //and stops a sound sample 2222 | * //when the user clicks the canvas 2223 | * 2224 | * //We will store the p5.MediaElement 2225 | * //object in here 2226 | * var ele; 2227 | * 2228 | * //while our audio is playing, 2229 | * //this will be set to true 2230 | * var sampleIsPlaying = false; 2231 | * 2232 | * function setup() { 2233 | * //Here we create a p5.MediaElement object 2234 | * //using the createAudio() function. 2235 | * ele = createAudio('assets/beat.mp3'); 2236 | * background(200); 2237 | * textAlign(CENTER); 2238 | * text('Click to play!', width / 2, height / 2); 2239 | * } 2240 | * 2241 | * function mouseClicked() { 2242 | * //here we test if the mouse is over the 2243 | * //canvas element when it's clicked 2244 | * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { 2245 | * background(200); 2246 | * 2247 | * if (sampleIsPlaying) { 2248 | * //if the sample is currently playing 2249 | * //calling the stop() function on 2250 | * //our p5.MediaElement will stop 2251 | * //it and reset its current 2252 | * //time to 0 (i.e. it will start 2253 | * //at the beginning the next time 2254 | * //you play it) 2255 | * ele.stop(); 2256 | * 2257 | * sampleIsPlaying = false; 2258 | * text('Click to play!', width / 2, height / 2); 2259 | * } else { 2260 | * //loop our sound element until we 2261 | * //call ele.stop() on it. 2262 | * ele.loop(); 2263 | * 2264 | * sampleIsPlaying = true; 2265 | * text('Click to stop!', width / 2, height / 2); 2266 | * } 2267 | * } 2268 | * } 2269 | *
2270 | */ 2271 | p5.MediaElement.prototype.stop = function() { 2272 | this.elt.pause(); 2273 | this.elt.currentTime = 0; 2274 | return this; 2275 | }; 2276 | 2277 | /** 2278 | * Pauses an HTML5 media element. 2279 | * 2280 | * @method pause 2281 | * @chainable 2282 | * @example 2283 | *
2284 | * //This example both starts 2285 | * //and pauses a sound sample 2286 | * //when the user clicks the canvas 2287 | * 2288 | * //We will store the p5.MediaElement 2289 | * //object in here 2290 | * var ele; 2291 | * 2292 | * //while our audio is playing, 2293 | * //this will be set to true 2294 | * var sampleIsPlaying = false; 2295 | * 2296 | * function setup() { 2297 | * //Here we create a p5.MediaElement object 2298 | * //using the createAudio() function. 2299 | * ele = createAudio('assets/lucky_dragons.mp3'); 2300 | * background(200); 2301 | * textAlign(CENTER); 2302 | * text('Click to play!', width / 2, height / 2); 2303 | * } 2304 | * 2305 | * function mouseClicked() { 2306 | * //here we test if the mouse is over the 2307 | * //canvas element when it's clicked 2308 | * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { 2309 | * background(200); 2310 | * 2311 | * if (sampleIsPlaying) { 2312 | * //Calling pause() on our 2313 | * //p5.MediaElement will stop it 2314 | * //playing, but when we call the 2315 | * //loop() or play() functions 2316 | * //the sample will start from 2317 | * //where we paused it. 2318 | * ele.pause(); 2319 | * 2320 | * sampleIsPlaying = false; 2321 | * text('Click to resume!', width / 2, height / 2); 2322 | * } else { 2323 | * //loop our sound element until we 2324 | * //call ele.pause() on it. 2325 | * ele.loop(); 2326 | * 2327 | * sampleIsPlaying = true; 2328 | * text('Click to pause!', width / 2, height / 2); 2329 | * } 2330 | * } 2331 | * } 2332 | *
2333 | */ 2334 | p5.MediaElement.prototype.pause = function() { 2335 | this.elt.pause(); 2336 | return this; 2337 | }; 2338 | 2339 | /** 2340 | * Set 'loop' to true for an HTML5 media element, and starts playing. 2341 | * 2342 | * @method loop 2343 | * @chainable 2344 | * @example 2345 | *
2346 | * //Clicking the canvas will loop 2347 | * //the audio sample until the user 2348 | * //clicks again to stop it 2349 | * 2350 | * //We will store the p5.MediaElement 2351 | * //object in here 2352 | * var ele; 2353 | * 2354 | * //while our audio is playing, 2355 | * //this will be set to true 2356 | * var sampleIsLooping = false; 2357 | * 2358 | * function setup() { 2359 | * //Here we create a p5.MediaElement object 2360 | * //using the createAudio() function. 2361 | * ele = createAudio('assets/lucky_dragons.mp3'); 2362 | * background(200); 2363 | * textAlign(CENTER); 2364 | * text('Click to loop!', width / 2, height / 2); 2365 | * } 2366 | * 2367 | * function mouseClicked() { 2368 | * //here we test if the mouse is over the 2369 | * //canvas element when it's clicked 2370 | * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { 2371 | * background(200); 2372 | * 2373 | * if (!sampleIsLooping) { 2374 | * //loop our sound element until we 2375 | * //call ele.stop() on it. 2376 | * ele.loop(); 2377 | * 2378 | * sampleIsLooping = true; 2379 | * text('Click to stop!', width / 2, height / 2); 2380 | * } else { 2381 | * ele.stop(); 2382 | * 2383 | * sampleIsLooping = false; 2384 | * text('Click to loop!', width / 2, height / 2); 2385 | * } 2386 | * } 2387 | * } 2388 | *
2389 | */ 2390 | p5.MediaElement.prototype.loop = function() { 2391 | this.elt.setAttribute('loop', true); 2392 | this.play(); 2393 | return this; 2394 | }; 2395 | /** 2396 | * Set 'loop' to false for an HTML5 media element. Element will stop 2397 | * when it reaches the end. 2398 | * 2399 | * @method noLoop 2400 | * @chainable 2401 | * @example 2402 | *
2403 | * //This example both starts 2404 | * //and stops loop of sound sample 2405 | * //when the user clicks the canvas 2406 | * 2407 | * //We will store the p5.MediaElement 2408 | * //object in here 2409 | * var ele; 2410 | * //while our audio is playing, 2411 | * //this will be set to true 2412 | * var sampleIsPlaying = false; 2413 | * 2414 | * function setup() { 2415 | * //Here we create a p5.MediaElement object 2416 | * //using the createAudio() function. 2417 | * ele = createAudio('assets/beat.mp3'); 2418 | * background(200); 2419 | * textAlign(CENTER); 2420 | * text('Click to play!', width / 2, height / 2); 2421 | * } 2422 | * 2423 | * function mouseClicked() { 2424 | * //here we test if the mouse is over the 2425 | * //canvas element when it's clicked 2426 | * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { 2427 | * background(200); 2428 | * 2429 | * if (sampleIsPlaying) { 2430 | * ele.noLoop(); 2431 | * text('No more Loops!', width / 2, height / 2); 2432 | * } else { 2433 | * ele.loop(); 2434 | * sampleIsPlaying = true; 2435 | * text('Click to stop looping!', width / 2, height / 2); 2436 | * } 2437 | * } 2438 | * } 2439 | *
2440 | * 2441 | */ 2442 | p5.MediaElement.prototype.noLoop = function() { 2443 | this.elt.setAttribute('loop', false); 2444 | return this; 2445 | }; 2446 | 2447 | /** 2448 | * Set HTML5 media element to autoplay or not. 2449 | * 2450 | * @method autoplay 2451 | * @param {Boolean} autoplay whether the element should autoplay 2452 | * @chainable 2453 | */ 2454 | p5.MediaElement.prototype.autoplay = function(val) { 2455 | this.elt.setAttribute('autoplay', val); 2456 | return this; 2457 | }; 2458 | 2459 | /** 2460 | * Sets volume for this HTML5 media element. If no argument is given, 2461 | * returns the current volume. 2462 | * 2463 | * @method volume 2464 | * @return {Number} current volume 2465 | * 2466 | * @example 2467 | *
2468 | * var ele; 2469 | * function setup() { 2470 | * // p5.MediaElement objects are usually created 2471 | * // by calling the createAudio(), createVideo(), 2472 | * // and createCapture() functions. 2473 | * // In this example we create 2474 | * // a new p5.MediaElement via createAudio(). 2475 | * ele = createAudio('assets/lucky_dragons.mp3'); 2476 | * background(250); 2477 | * textAlign(CENTER); 2478 | * text('Click to Play!', width / 2, height / 2); 2479 | * } 2480 | * function mouseClicked() { 2481 | * // Here we call the volume() function 2482 | * // on the sound element to set its volume 2483 | * // Volume must be between 0.0 and 1.0 2484 | * ele.volume(0.2); 2485 | * ele.play(); 2486 | * background(200); 2487 | * text('You clicked Play!', width / 2, height / 2); 2488 | * } 2489 | *
2490 | *
2491 | * var audio; 2492 | * var counter = 0; 2493 | * 2494 | * function loaded() { 2495 | * audio.play(); 2496 | * } 2497 | * 2498 | * function setup() { 2499 | * audio = createAudio('assets/lucky_dragons.mp3', loaded); 2500 | * textAlign(CENTER); 2501 | * } 2502 | * 2503 | * function draw() { 2504 | * if (counter === 0) { 2505 | * background(0, 255, 0); 2506 | * text('volume(0.9)', width / 2, height / 2); 2507 | * } else if (counter === 1) { 2508 | * background(255, 255, 0); 2509 | * text('volume(0.5)', width / 2, height / 2); 2510 | * } else if (counter === 2) { 2511 | * background(255, 0, 0); 2512 | * text('volume(0.1)', width / 2, height / 2); 2513 | * } 2514 | * } 2515 | * 2516 | * function mousePressed() { 2517 | * counter++; 2518 | * if (counter === 0) { 2519 | * audio.volume(0.9); 2520 | * } else if (counter === 1) { 2521 | * audio.volume(0.5); 2522 | * } else if (counter === 2) { 2523 | * audio.volume(0.1); 2524 | * } else { 2525 | * counter = 0; 2526 | * audio.volume(0.9); 2527 | * } 2528 | * } 2529 | * 2530 | *
2531 | */ 2532 | /** 2533 | * @method volume 2534 | * @param {Number} val volume between 0.0 and 1.0 2535 | * @chainable 2536 | */ 2537 | p5.MediaElement.prototype.volume = function(val) { 2538 | if (typeof val === 'undefined') { 2539 | return this.elt.volume; 2540 | } else { 2541 | this.elt.volume = val; 2542 | } 2543 | }; 2544 | 2545 | /** 2546 | * If no arguments are given, returns the current playback speed of the 2547 | * element. The speed parameter sets the speed where 2.0 will play the 2548 | * element twice as fast, 0.5 will play at half the speed, and -1 will play 2549 | * the element in normal speed in reverse.(Note that not all browsers support 2550 | * backward playback and even if they do, playback might not be smooth.) 2551 | * 2552 | * @method speed 2553 | * @return {Number} current playback speed of the element 2554 | * 2555 | * @example 2556 | *
2557 | * //Clicking the canvas will loop 2558 | * //the audio sample until the user 2559 | * //clicks again to stop it 2560 | * 2561 | * //We will store the p5.MediaElement 2562 | * //object in here 2563 | * var ele; 2564 | * var button; 2565 | * 2566 | * function setup() { 2567 | * createCanvas(710, 400); 2568 | * //Here we create a p5.MediaElement object 2569 | * //using the createAudio() function. 2570 | * ele = createAudio('assets/beat.mp3'); 2571 | * ele.loop(); 2572 | * background(200); 2573 | * 2574 | * button = createButton('2x speed'); 2575 | * button.position(100, 68); 2576 | * button.mousePressed(twice_speed); 2577 | * 2578 | * button = createButton('half speed'); 2579 | * button.position(200, 68); 2580 | * button.mousePressed(half_speed); 2581 | * 2582 | * button = createButton('reverse play'); 2583 | * button.position(300, 68); 2584 | * button.mousePressed(reverse_speed); 2585 | * 2586 | * button = createButton('STOP'); 2587 | * button.position(400, 68); 2588 | * button.mousePressed(stop_song); 2589 | * 2590 | * button = createButton('PLAY!'); 2591 | * button.position(500, 68); 2592 | * button.mousePressed(play_speed); 2593 | * } 2594 | * 2595 | * function twice_speed() { 2596 | * ele.speed(2); 2597 | * } 2598 | * 2599 | * function half_speed() { 2600 | * ele.speed(0.5); 2601 | * } 2602 | * 2603 | * function reverse_speed() { 2604 | * ele.speed(-1); 2605 | * } 2606 | * 2607 | * function stop_song() { 2608 | * ele.stop(); 2609 | * } 2610 | * 2611 | * function play_speed() { 2612 | * ele.play(); 2613 | * } 2614 | *
2615 | */ 2616 | /** 2617 | * @method speed 2618 | * @param {Number} speed speed multiplier for element playback 2619 | * @chainable 2620 | */ 2621 | p5.MediaElement.prototype.speed = function(val) { 2622 | if (typeof val === 'undefined') { 2623 | return this.presetPlaybackRate || this.elt.playbackRate; 2624 | } else { 2625 | if (this.loadedmetadata) { 2626 | this.elt.playbackRate = val; 2627 | } else { 2628 | this.presetPlaybackRate = val; 2629 | } 2630 | } 2631 | }; 2632 | 2633 | /** 2634 | * If no arguments are given, returns the current time of the element. 2635 | * If an argument is given the current time of the element is set to it. 2636 | * 2637 | * @method time 2638 | * @return {Number} current time (in seconds) 2639 | * 2640 | * @example 2641 | *
2642 | * var ele; 2643 | * var beginning = true; 2644 | * function setup() { 2645 | * //p5.MediaElement objects are usually created 2646 | * //by calling the createAudio(), createVideo(), 2647 | * //and createCapture() functions. 2648 | * 2649 | * //In this example we create 2650 | * //a new p5.MediaElement via createAudio(). 2651 | * ele = createAudio('assets/lucky_dragons.mp3'); 2652 | * background(250); 2653 | * textAlign(CENTER); 2654 | * text('start at beginning', width / 2, height / 2); 2655 | * } 2656 | * 2657 | * // this function fires with click anywhere 2658 | * function mousePressed() { 2659 | * if (beginning === true) { 2660 | * // here we start the sound at the beginning 2661 | * // time(0) is not necessary here 2662 | * // as this produces the same result as 2663 | * // play() 2664 | * ele.play().time(0); 2665 | * background(200); 2666 | * text('jump 2 sec in', width / 2, height / 2); 2667 | * beginning = false; 2668 | * } else { 2669 | * // here we jump 2 seconds into the sound 2670 | * ele.play().time(2); 2671 | * background(250); 2672 | * text('start at beginning', width / 2, height / 2); 2673 | * beginning = true; 2674 | * } 2675 | * } 2676 | *
2677 | */ 2678 | /** 2679 | * @method time 2680 | * @param {Number} time time to jump to (in seconds) 2681 | * @chainable 2682 | */ 2683 | p5.MediaElement.prototype.time = function(val) { 2684 | if (typeof val === 'undefined') { 2685 | return this.elt.currentTime; 2686 | } else { 2687 | this.elt.currentTime = val; 2688 | return this; 2689 | } 2690 | }; 2691 | 2692 | /** 2693 | * Returns the duration of the HTML5 media element. 2694 | * 2695 | * @method duration 2696 | * @return {Number} duration 2697 | * 2698 | * @example 2699 | *
2700 | * var ele; 2701 | * function setup() { 2702 | * //p5.MediaElement objects are usually created 2703 | * //by calling the createAudio(), createVideo(), 2704 | * //and createCapture() functions. 2705 | * //In this example we create 2706 | * //a new p5.MediaElement via createAudio(). 2707 | * ele = createAudio('assets/doorbell.mp3'); 2708 | * background(250); 2709 | * textAlign(CENTER); 2710 | * text('Click to know the duration!', 10, 25, 70, 80); 2711 | * } 2712 | * function mouseClicked() { 2713 | * ele.play(); 2714 | * background(200); 2715 | * //ele.duration dislpays the duration 2716 | * text(ele.duration() + ' seconds', width / 2, height / 2); 2717 | * } 2718 | *
2719 | */ 2720 | p5.MediaElement.prototype.duration = function() { 2721 | return this.elt.duration; 2722 | }; 2723 | p5.MediaElement.prototype.pixels = []; 2724 | p5.MediaElement.prototype._ensureCanvas = function() { 2725 | if (!this.canvas) this.loadPixels(); 2726 | }; 2727 | p5.MediaElement.prototype.loadPixels = function() { 2728 | if (!this.canvas) { 2729 | this.canvas = document.createElement('canvas'); 2730 | this.drawingContext = this.canvas.getContext('2d'); 2731 | } 2732 | if (this.loadedmetadata) { 2733 | // wait for metadata for w/h 2734 | if (this.canvas.width !== this.elt.width) { 2735 | this.canvas.width = this.elt.width; 2736 | this.canvas.height = this.elt.height; 2737 | this.width = this.canvas.width; 2738 | this.height = this.canvas.height; 2739 | this._pixelsDirty = true; 2740 | } 2741 | 2742 | var currentTime = this.elt.currentTime; 2743 | if (this._pixelsDirty || this._pixelsTime !== currentTime) { 2744 | // only update the pixels array if it's dirty, or 2745 | // if the video time has changed. 2746 | this._pixelsTime = currentTime; 2747 | this._pixelsDirty = true; 2748 | 2749 | this.drawingContext.drawImage( 2750 | this.elt, 2751 | 0, 2752 | 0, 2753 | this.canvas.width, 2754 | this.canvas.height 2755 | ); 2756 | p5.Renderer2D.prototype.loadPixels.call(this); 2757 | } 2758 | } 2759 | this.setModified(true); 2760 | return this; 2761 | }; 2762 | p5.MediaElement.prototype.updatePixels = function(x, y, w, h) { 2763 | if (this.loadedmetadata) { 2764 | // wait for metadata 2765 | this._ensureCanvas(); 2766 | p5.Renderer2D.prototype.updatePixels.call(this, x, y, w, h); 2767 | } 2768 | this.setModified(true); 2769 | return this; 2770 | }; 2771 | p5.MediaElement.prototype.get = function(x, y, w, h) { 2772 | if (this.loadedmetadata) { 2773 | // wait for metadata 2774 | var currentTime = this.elt.currentTime; 2775 | if (this._pixelsTime !== currentTime) { 2776 | this.loadPixels(); 2777 | } else { 2778 | this._ensureCanvas(); 2779 | } 2780 | 2781 | return p5.Renderer2D.prototype.get.call(this, x, y, w, h); 2782 | } else if (typeof x === 'undefined') { 2783 | return new p5.Image(1, 1); 2784 | } else if (w > 1) { 2785 | return new p5.Image(x, y, w, h); 2786 | } else { 2787 | return [0, 0, 0, 255]; 2788 | } 2789 | }; 2790 | p5.MediaElement.prototype.set = function(x, y, imgOrCol) { 2791 | if (this.loadedmetadata) { 2792 | // wait for metadata 2793 | this._ensureCanvas(); 2794 | p5.Renderer2D.prototype.set.call(this, x, y, imgOrCol); 2795 | this.setModified(true); 2796 | } 2797 | }; 2798 | p5.MediaElement.prototype.copy = function() { 2799 | this._ensureCanvas(); 2800 | p5.Renderer2D.prototype.copy.apply(this, arguments); 2801 | }; 2802 | p5.MediaElement.prototype.mask = function() { 2803 | this.loadPixels(); 2804 | this.setModified(true); 2805 | p5.Image.prototype.mask.apply(this, arguments); 2806 | }; 2807 | /** 2808 | * helper method for web GL mode to figure out if the element 2809 | * has been modified and might need to be re-uploaded to texture 2810 | * memory between frames. 2811 | * @method isModified 2812 | * @private 2813 | * @return {boolean} a boolean indicating whether or not the 2814 | * image has been updated or modified since last texture upload. 2815 | */ 2816 | p5.MediaElement.prototype.isModified = function() { 2817 | return this._modified; 2818 | }; 2819 | /** 2820 | * helper method for web GL mode to indicate that an element has been 2821 | * changed or unchanged since last upload. gl texture upload will 2822 | * set this value to false after uploading the texture; or might set 2823 | * it to true if metadata has become available but there is no actual 2824 | * texture data available yet.. 2825 | * @method setModified 2826 | * @param {boolean} val sets whether or not the element has been 2827 | * modified. 2828 | * @private 2829 | */ 2830 | p5.MediaElement.prototype.setModified = function(value) { 2831 | this._modified = value; 2832 | }; 2833 | /** 2834 | * Schedule an event to be called when the audio or video 2835 | * element reaches the end. If the element is looping, 2836 | * this will not be called. The element is passed in 2837 | * as the argument to the onended callback. 2838 | * 2839 | * @method onended 2840 | * @param {Function} callback function to call when the 2841 | * soundfile has ended. The 2842 | * media element will be passed 2843 | * in as the argument to the 2844 | * callback. 2845 | * @chainable 2846 | * @example 2847 | *
2848 | * function setup() { 2849 | * var audioEl = createAudio('assets/beat.mp3'); 2850 | * audioEl.showControls(); 2851 | * audioEl.onended(sayDone); 2852 | * } 2853 | * 2854 | * function sayDone(elt) { 2855 | * alert('done playing ' + elt.src); 2856 | * } 2857 | *
2858 | */ 2859 | p5.MediaElement.prototype.onended = function(callback) { 2860 | this._onended = callback; 2861 | return this; 2862 | }; 2863 | 2864 | /*** CONNECT TO WEB AUDIO API / p5.sound.js ***/ 2865 | 2866 | /** 2867 | * Send the audio output of this element to a specified audioNode or 2868 | * p5.sound object. If no element is provided, connects to p5's master 2869 | * output. That connection is established when this method is first called. 2870 | * All connections are removed by the .disconnect() method. 2871 | * 2872 | * This method is meant to be used with the p5.sound.js addon library. 2873 | * 2874 | * @method connect 2875 | * @param {AudioNode|Object} audioNode AudioNode from the Web Audio API, 2876 | * or an object from the p5.sound library 2877 | */ 2878 | p5.MediaElement.prototype.connect = function(obj) { 2879 | var audioContext, masterOutput; 2880 | 2881 | // if p5.sound exists, same audio context 2882 | if (typeof p5.prototype.getAudioContext === 'function') { 2883 | audioContext = p5.prototype.getAudioContext(); 2884 | masterOutput = p5.soundOut.input; 2885 | } else { 2886 | try { 2887 | audioContext = obj.context; 2888 | masterOutput = audioContext.destination; 2889 | } catch (e) { 2890 | throw 'connect() is meant to be used with Web Audio API or p5.sound.js'; 2891 | } 2892 | } 2893 | 2894 | // create a Web Audio MediaElementAudioSourceNode if none already exists 2895 | if (!this.audioSourceNode) { 2896 | this.audioSourceNode = audioContext.createMediaElementSource(this.elt); 2897 | 2898 | // connect to master output when this method is first called 2899 | this.audioSourceNode.connect(masterOutput); 2900 | } 2901 | 2902 | // connect to object if provided 2903 | if (obj) { 2904 | if (obj.input) { 2905 | this.audioSourceNode.connect(obj.input); 2906 | } else { 2907 | this.audioSourceNode.connect(obj); 2908 | } 2909 | } else { 2910 | // otherwise connect to master output of p5.sound / AudioContext 2911 | this.audioSourceNode.connect(masterOutput); 2912 | } 2913 | }; 2914 | 2915 | /** 2916 | * Disconnect all Web Audio routing, including to master output. 2917 | * This is useful if you want to re-route the output through 2918 | * audio effects, for example. 2919 | * 2920 | * @method disconnect 2921 | */ 2922 | p5.MediaElement.prototype.disconnect = function() { 2923 | if (this.audioSourceNode) { 2924 | this.audioSourceNode.disconnect(); 2925 | } else { 2926 | throw 'nothing to disconnect'; 2927 | } 2928 | }; 2929 | 2930 | /*** SHOW / HIDE CONTROLS ***/ 2931 | 2932 | /** 2933 | * Show the default MediaElement controls, as determined by the web browser. 2934 | * 2935 | * @method showControls 2936 | * @example 2937 | *
2938 | * var ele; 2939 | * function setup() { 2940 | * //p5.MediaElement objects are usually created 2941 | * //by calling the createAudio(), createVideo(), 2942 | * //and createCapture() functions. 2943 | * //In this example we create 2944 | * //a new p5.MediaElement via createAudio() 2945 | * ele = createAudio('assets/lucky_dragons.mp3'); 2946 | * background(200); 2947 | * textAlign(CENTER); 2948 | * text('Click to Show Controls!', 10, 25, 70, 80); 2949 | * } 2950 | * function mousePressed() { 2951 | * ele.showControls(); 2952 | * background(200); 2953 | * text('Controls Shown', width / 2, height / 2); 2954 | * } 2955 | *
2956 | */ 2957 | p5.MediaElement.prototype.showControls = function() { 2958 | // must set style for the element to show on the page 2959 | this.elt.style['text-align'] = 'inherit'; 2960 | this.elt.controls = true; 2961 | }; 2962 | 2963 | /** 2964 | * Hide the default mediaElement controls. 2965 | * @method hideControls 2966 | * @example 2967 | *
2968 | * var ele; 2969 | * function setup() { 2970 | * //p5.MediaElement objects are usually created 2971 | * //by calling the createAudio(), createVideo(), 2972 | * //and createCapture() functions. 2973 | * //In this example we create 2974 | * //a new p5.MediaElement via createAudio() 2975 | * ele = createAudio('assets/lucky_dragons.mp3'); 2976 | * ele.showControls(); 2977 | * background(200); 2978 | * textAlign(CENTER); 2979 | * text('Click to hide Controls!', 10, 25, 70, 80); 2980 | * } 2981 | * function mousePressed() { 2982 | * ele.hideControls(); 2983 | * background(200); 2984 | * text('Controls hidden', width / 2, height / 2); 2985 | * } 2986 | *
2987 | */ 2988 | p5.MediaElement.prototype.hideControls = function() { 2989 | this.elt.controls = false; 2990 | }; 2991 | 2992 | /*** SCHEDULE EVENTS ***/ 2993 | 2994 | // Cue inspired by JavaScript setTimeout, and the 2995 | // Tone.js Transport Timeline Event, MIT License Yotam Mann 2015 tonejs.org 2996 | var Cue = function(callback, time, id, val) { 2997 | this.callback = callback; 2998 | this.time = time; 2999 | this.id = id; 3000 | this.val = val; 3001 | }; 3002 | 3003 | /** 3004 | * Schedule events to trigger every time a MediaElement 3005 | * (audio/video) reaches a playback cue point. 3006 | * 3007 | * Accepts a callback function, a time (in seconds) at which to trigger 3008 | * the callback, and an optional parameter for the callback. 3009 | * 3010 | * Time will be passed as the first parameter to the callback function, 3011 | * and param will be the second parameter. 3012 | * 3013 | * 3014 | * @method addCue 3015 | * @param {Number} time Time in seconds, relative to this media 3016 | * element's playback. For example, to trigger 3017 | * an event every time playback reaches two 3018 | * seconds, pass in the number 2. This will be 3019 | * passed as the first parameter to 3020 | * the callback function. 3021 | * @param {Function} callback Name of a function that will be 3022 | * called at the given time. The callback will 3023 | * receive time and (optionally) param as its 3024 | * two parameters. 3025 | * @param {Object} [value] An object to be passed as the 3026 | * second parameter to the 3027 | * callback function. 3028 | * @return {Number} id ID of this cue, 3029 | * useful for removeCue(id) 3030 | * @example 3031 | *
3032 | * // 3033 | * // 3034 | * function setup() { 3035 | * noCanvas(); 3036 | * 3037 | * var audioEl = createAudio('assets/beat.mp3'); 3038 | * audioEl.showControls(); 3039 | * 3040 | * // schedule three calls to changeBackground 3041 | * audioEl.addCue(0.5, changeBackground, color(255, 0, 0)); 3042 | * audioEl.addCue(1.0, changeBackground, color(0, 255, 0)); 3043 | * audioEl.addCue(2.5, changeBackground, color(0, 0, 255)); 3044 | * audioEl.addCue(3.0, changeBackground, color(0, 255, 255)); 3045 | * audioEl.addCue(4.2, changeBackground, color(255, 255, 0)); 3046 | * audioEl.addCue(5.0, changeBackground, color(255, 255, 0)); 3047 | * } 3048 | * 3049 | * function changeBackground(val) { 3050 | * background(val); 3051 | * } 3052 | *
3053 | */ 3054 | p5.MediaElement.prototype.addCue = function(time, callback, val) { 3055 | var id = this._cueIDCounter++; 3056 | 3057 | var cue = new Cue(callback, time, id, val); 3058 | this._cues.push(cue); 3059 | 3060 | if (!this.elt.ontimeupdate) { 3061 | this.elt.ontimeupdate = this._onTimeUpdate.bind(this); 3062 | } 3063 | 3064 | return id; 3065 | }; 3066 | 3067 | /** 3068 | * Remove a callback based on its ID. The ID is returned by the 3069 | * addCue method. 3070 | * @method removeCue 3071 | * @param {Number} id ID of the cue, as returned by addCue 3072 | * @example 3073 | *
3074 | * var audioEl, id1, id2; 3075 | * function setup() { 3076 | * background(255, 255, 255); 3077 | * audioEl = createAudio('assets/beat.mp3'); 3078 | * audioEl.showControls(); 3079 | * // schedule five calls to changeBackground 3080 | * id1 = audioEl.addCue(0.5, changeBackground, color(255, 0, 0)); 3081 | * audioEl.addCue(1.0, changeBackground, color(0, 255, 0)); 3082 | * audioEl.addCue(2.5, changeBackground, color(0, 0, 255)); 3083 | * audioEl.addCue(3.0, changeBackground, color(0, 255, 255)); 3084 | * id2 = audioEl.addCue(4.2, changeBackground, color(255, 255, 0)); 3085 | * text('Click to remove first and last Cue!', 10, 25, 70, 80); 3086 | * } 3087 | * function mousePressed() { 3088 | * audioEl.removeCue(id1); 3089 | * audioEl.removeCue(id2); 3090 | * } 3091 | * function changeBackground(val) { 3092 | * background(val); 3093 | * } 3094 | *
3095 | */ 3096 | p5.MediaElement.prototype.removeCue = function(id) { 3097 | for (var i = 0; i < this._cues.length; i++) { 3098 | if (this._cues[i].id === id) { 3099 | console.log(id); 3100 | this._cues.splice(i, 1); 3101 | } 3102 | } 3103 | 3104 | if (this._cues.length === 0) { 3105 | this.elt.ontimeupdate = null; 3106 | } 3107 | }; 3108 | 3109 | /** 3110 | * Remove all of the callbacks that had originally been scheduled 3111 | * via the addCue method. 3112 | * @method clearCues 3113 | * @param {Number} id ID of the cue, as returned by addCue 3114 | * @example 3115 | *
3116 | * var audioEl; 3117 | * function setup() { 3118 | * background(255, 255, 255); 3119 | * audioEl = createAudio('assets/beat.mp3'); 3120 | * //Show the default MediaElement controls, as determined by the web browser 3121 | * audioEl.showControls(); 3122 | * // schedule calls to changeBackground 3123 | * background(200); 3124 | * text('Click to change Cue!', 10, 25, 70, 80); 3125 | * audioEl.addCue(0.5, changeBackground, color(255, 0, 0)); 3126 | * audioEl.addCue(1.0, changeBackground, color(0, 255, 0)); 3127 | * audioEl.addCue(2.5, changeBackground, color(0, 0, 255)); 3128 | * audioEl.addCue(3.0, changeBackground, color(0, 255, 255)); 3129 | * audioEl.addCue(4.2, changeBackground, color(255, 255, 0)); 3130 | * } 3131 | * function mousePressed() { 3132 | * // here we clear the scheduled callbacks 3133 | * audioEl.clearCues(); 3134 | * // then we add some more callbacks 3135 | * audioEl.addCue(1, changeBackground, color(2, 2, 2)); 3136 | * audioEl.addCue(3, changeBackground, color(255, 255, 0)); 3137 | * } 3138 | * function changeBackground(val) { 3139 | * background(val); 3140 | * } 3141 | *
3142 | */ 3143 | p5.MediaElement.prototype.clearCues = function() { 3144 | this._cues = []; 3145 | this.elt.ontimeupdate = null; 3146 | }; 3147 | 3148 | // private method that checks for cues to be fired if events 3149 | // have been scheduled using addCue(callback, time). 3150 | p5.MediaElement.prototype._onTimeUpdate = function() { 3151 | var playbackTime = this.time(); 3152 | 3153 | for (var i = 0; i < this._cues.length; i++) { 3154 | var callbackTime = this._cues[i].time; 3155 | var val = this._cues[i].val; 3156 | 3157 | if (this._prevTime < callbackTime && callbackTime <= playbackTime) { 3158 | // pass the scheduled callbackTime as parameter to the callback 3159 | this._cues[i].callback(val); 3160 | } 3161 | } 3162 | 3163 | this._prevTime = playbackTime; 3164 | }; 3165 | 3166 | // ============================================================================= 3167 | // p5.File 3168 | // ============================================================================= 3169 | 3170 | /** 3171 | * Base class for a file 3172 | * Using this for createFileInput 3173 | * 3174 | * @class p5.File 3175 | * @constructor 3176 | * @param {File} file File that is wrapped 3177 | */ 3178 | p5.File = function(file, pInst) { 3179 | /** 3180 | * Underlying File object. All normal File methods can be called on this. 3181 | * 3182 | * @property file 3183 | */ 3184 | this.file = file; 3185 | 3186 | this._pInst = pInst; 3187 | 3188 | // Splitting out the file type into two components 3189 | // This makes determining if image or text etc simpler 3190 | var typeList = file.type.split('/'); 3191 | /** 3192 | * File type (image, text, etc.) 3193 | * 3194 | * @property type 3195 | */ 3196 | this.type = typeList[0]; 3197 | /** 3198 | * File subtype (usually the file extension jpg, png, xml, etc.) 3199 | * 3200 | * @property subtype 3201 | */ 3202 | this.subtype = typeList[1]; 3203 | /** 3204 | * File name 3205 | * 3206 | * @property name 3207 | */ 3208 | this.name = file.name; 3209 | /** 3210 | * File size 3211 | * 3212 | * @property size 3213 | */ 3214 | this.size = file.size; 3215 | 3216 | /** 3217 | * URL string containing image data. 3218 | * 3219 | * @property data 3220 | */ 3221 | this.data = undefined; 3222 | }; 3223 | }); 3224 | 3225 | p5.File._createLoader = function(theFile, callback) { 3226 | var reader = new FileReader(); 3227 | reader.onload = function(e) { 3228 | var p5file = new p5.File(theFile); 3229 | p5file.data = e.target.result; 3230 | callback(p5file); 3231 | }; 3232 | return reader; 3233 | }; 3234 | 3235 | p5.File._load = function(f, callback) { 3236 | // Text or data? 3237 | // This should likely be improved 3238 | if (/^text\//.test(f.type)) { 3239 | p5.File._createLoader(f, callback).readAsText(f); 3240 | } else if (!/^(video|audio)\//.test(f.type)) { 3241 | p5.File._createLoader(f, callback).readAsDataURL(f); 3242 | } else { 3243 | var file = new p5.File(f); 3244 | file.data = URL.createObjectURL(f); 3245 | callback(file); 3246 | } 3247 | }; 3248 | -------------------------------------------------------------------------------- /libs/p5.gif.js: -------------------------------------------------------------------------------- 1 | // this is a slightly modified version of buzzfeed's SuperGif (https://github.com/buzzfeed/libgif-js) 2 | // which is based on shachaf's jsgif (https://github.com/shachaf/jsgif) 3 | (function(root, factory) { 4 | if (typeof define === 'function' && define.amd) { 5 | define([], factory); 6 | } else if (typeof exports === 'object') { 7 | module.exports = factory(); 8 | } else { 9 | root.SuperGif = factory(); 10 | } 11 | }(this, function() { 12 | // Generic functions 13 | var bitsToNum = function(ba) { 14 | return ba.reduce(function(s, n) { 15 | return s * 2 + n; 16 | }, 0); 17 | }; 18 | 19 | var byteToBitArr = function(bite) { 20 | var a = []; 21 | for (var i = 7; i >= 0; i--) { 22 | a.push(!!(bite & (1 << i))); 23 | } 24 | return a; 25 | }; 26 | 27 | // Stream 28 | /** 29 | * @constructor 30 | */ 31 | // Make compiler happy. 32 | var Stream = function(data) { 33 | this.data = data; 34 | this.len = this.data.length; 35 | this.pos = 0; 36 | 37 | this.readByte = function() { 38 | if (this.pos >= this.data.length) { 39 | throw new Error('Attempted to read past end of stream.'); 40 | } 41 | if (data instanceof Uint8Array) 42 | return data[this.pos++]; 43 | else 44 | return data.charCodeAt(this.pos++) & 0xFF; 45 | }; 46 | 47 | this.readBytes = function(n) { 48 | var bytes = []; 49 | for (var i = 0; i < n; i++) { 50 | bytes.push(this.readByte()); 51 | } 52 | return bytes; 53 | }; 54 | 55 | this.read = function(n) { 56 | var s = ''; 57 | for (var i = 0; i < n; i++) { 58 | s += String.fromCharCode(this.readByte()); 59 | } 60 | return s; 61 | }; 62 | 63 | this.readUnsigned = function() { // Little-endian. 64 | var a = this.readBytes(2); 65 | return (a[1] << 8) + a[0]; 66 | }; 67 | }; 68 | 69 | var lzwDecode = function(minCodeSize, data) { 70 | // TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String? 71 | var pos = 0; // Maybe this streaming thing should be merged with the Stream? 72 | var readCode = function(size) { 73 | var code = 0; 74 | for (var i = 0; i < size; i++) { 75 | if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) { 76 | code |= 1 << i; 77 | } 78 | pos++; 79 | } 80 | return code; 81 | }; 82 | 83 | var output = []; 84 | 85 | var clearCode = 1 << minCodeSize; 86 | var eoiCode = clearCode + 1; 87 | 88 | var codeSize = minCodeSize + 1; 89 | 90 | var dict = []; 91 | 92 | var clear = function() { 93 | dict = []; 94 | codeSize = minCodeSize + 1; 95 | for (var i = 0; i < clearCode; i++) { 96 | dict[i] = [i]; 97 | } 98 | dict[clearCode] = []; 99 | dict[eoiCode] = null; 100 | 101 | }; 102 | 103 | var code; 104 | var last; 105 | 106 | while (true) { 107 | last = code; 108 | code = readCode(codeSize); 109 | 110 | if (code === clearCode) { 111 | clear(); 112 | continue; 113 | } 114 | if (code === eoiCode) break; 115 | 116 | if (code < dict.length) { 117 | if (last !== clearCode) { 118 | dict.push(dict[last].concat(dict[code][0])); 119 | } 120 | } else { 121 | if (code !== dict.length) throw new Error('Invalid LZW code.'); 122 | dict.push(dict[last].concat(dict[last][0])); 123 | } 124 | output.push.apply(output, dict[code]); 125 | 126 | if (dict.length === (1 << codeSize) && codeSize < 12) { 127 | // If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long. 128 | codeSize++; 129 | } 130 | } 131 | 132 | // I don't know if this is technically an error, but some GIFs do it. 133 | //if (Math.ceil(pos / 8) !== data.length) throw new Error('Extraneous LZW bytes.'); 134 | return output; 135 | }; 136 | 137 | 138 | // The actual parsing; returns an object with properties. 139 | var parseGIF = function(st, handler) { 140 | handler || (handler = {}); 141 | 142 | // LZW (GIF-specific) 143 | var parseCT = function(entries) { // Each entry is 3 bytes, for RGB. 144 | var ct = []; 145 | for (var i = 0; i < entries; i++) { 146 | ct.push(st.readBytes(3)); 147 | } 148 | return ct; 149 | }; 150 | 151 | var readSubBlocks = function() { 152 | var size, data; 153 | data = ''; 154 | do { 155 | size = st.readByte(); 156 | data += st.read(size); 157 | } while (size !== 0); 158 | return data; 159 | }; 160 | 161 | var parseHeader = function() { 162 | var hdr = {}; 163 | hdr.sig = st.read(3); 164 | hdr.ver = st.read(3); 165 | if (hdr.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely. 166 | hdr.width = st.readUnsigned(); 167 | hdr.height = st.readUnsigned(); 168 | 169 | var bits = byteToBitArr(st.readByte()); 170 | hdr.gctFlag = bits.shift(); 171 | hdr.colorRes = bitsToNum(bits.splice(0, 3)); 172 | hdr.sorted = bits.shift(); 173 | hdr.gctSize = bitsToNum(bits.splice(0, 3)); 174 | 175 | hdr.bgColor = st.readByte(); 176 | hdr.pixelAspectRatio = st.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64 177 | if (hdr.gctFlag) { 178 | hdr.gct = parseCT(1 << (hdr.gctSize + 1)); 179 | } 180 | handler.hdr && handler.hdr(hdr); 181 | }; 182 | 183 | var parseExt = function(block) { 184 | var parseGCExt = function(block) { 185 | var blockSize = st.readByte(); // Always 4 186 | var bits = byteToBitArr(st.readByte()); 187 | block.reserved = bits.splice(0, 3); // Reserved; should be 000. 188 | block.disposalMethod = bitsToNum(bits.splice(0, 3)); 189 | block.userInput = bits.shift(); 190 | block.transparencyGiven = bits.shift(); 191 | 192 | block.delayTime = st.readUnsigned(); 193 | 194 | block.transparencyIndex = st.readByte(); 195 | 196 | block.terminator = st.readByte(); 197 | 198 | handler.gce && handler.gce(block); 199 | }; 200 | 201 | var parseComExt = function(block) { 202 | block.comment = readSubBlocks(); 203 | handler.com && handler.com(block); 204 | }; 205 | 206 | var parsePTExt = function(block) { 207 | // No one *ever* uses this. If you use it, deal with parsing it yourself. 208 | var blockSize = st.readByte(); // Always 12 209 | block.ptHeader = st.readBytes(12); 210 | block.ptData = readSubBlocks(); 211 | handler.pte && handler.pte(block); 212 | }; 213 | 214 | var parseAppExt = function(block) { 215 | var parseNetscapeExt = function(block) { 216 | var blockSize = st.readByte(); // Always 3 217 | block.unknown = st.readByte(); // ??? Always 1? What is this? 218 | block.iterations = st.readUnsigned(); 219 | block.terminator = st.readByte(); 220 | handler.app && handler.app.NETSCAPE && handler.app.NETSCAPE(block); 221 | }; 222 | 223 | var parseUnknownAppExt = function(block) { 224 | block.appData = readSubBlocks(); 225 | // FIXME: This won't work if a handler wants to match on any identifier. 226 | handler.app && handler.app[block.identifier] && handler.app[block.identifier](block); 227 | }; 228 | 229 | var blockSize = st.readByte(); // Always 11 230 | block.identifier = st.read(8); 231 | block.authCode = st.read(3); 232 | switch (block.identifier) { 233 | case 'NETSCAPE': 234 | parseNetscapeExt(block); 235 | break; 236 | default: 237 | parseUnknownAppExt(block); 238 | break; 239 | } 240 | }; 241 | 242 | var parseUnknownExt = function(block) { 243 | block.data = readSubBlocks(); 244 | handler.unknown && handler.unknown(block); 245 | }; 246 | 247 | block.label = st.readByte(); 248 | switch (block.label) { 249 | case 0xF9: 250 | block.extType = 'gce'; 251 | parseGCExt(block); 252 | break; 253 | case 0xFE: 254 | block.extType = 'com'; 255 | parseComExt(block); 256 | break; 257 | case 0x01: 258 | block.extType = 'pte'; 259 | parsePTExt(block); 260 | break; 261 | case 0xFF: 262 | block.extType = 'app'; 263 | parseAppExt(block); 264 | break; 265 | default: 266 | block.extType = 'unknown'; 267 | parseUnknownExt(block); 268 | break; 269 | } 270 | }; 271 | 272 | var parseImg = function(img) { 273 | var deinterlace = function(pixels, width) { 274 | // Of course this defeats the purpose of interlacing. And it's *probably* 275 | // the least efficient way it's ever been implemented. But nevertheless... 276 | var newPixels = new Array(pixels.length); 277 | var rows = pixels.length / width; 278 | var cpRow = function(toRow, fromRow) { 279 | var fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width); 280 | newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels)); 281 | }; 282 | 283 | // See appendix E. 284 | var offsets = [0, 4, 2, 1]; 285 | var steps = [8, 8, 4, 2]; 286 | 287 | var fromRow = 0; 288 | for (var pass = 0; pass < 4; pass++) { 289 | for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) { 290 | cpRow(toRow, fromRow); 291 | fromRow++; 292 | } 293 | } 294 | 295 | return newPixels; 296 | }; 297 | 298 | img.leftPos = st.readUnsigned(); 299 | img.topPos = st.readUnsigned(); 300 | img.width = st.readUnsigned(); 301 | img.height = st.readUnsigned(); 302 | 303 | var bits = byteToBitArr(st.readByte()); 304 | img.lctFlag = bits.shift(); 305 | img.interlaced = bits.shift(); 306 | img.sorted = bits.shift(); 307 | img.reserved = bits.splice(0, 2); 308 | img.lctSize = bitsToNum(bits.splice(0, 3)); 309 | 310 | if (img.lctFlag) { 311 | img.lct = parseCT(1 << (img.lctSize + 1)); 312 | } 313 | 314 | img.lzwMinCodeSize = st.readByte(); 315 | 316 | var lzwData = readSubBlocks(); 317 | 318 | img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData); 319 | 320 | if (img.interlaced) { // Move 321 | img.pixels = deinterlace(img.pixels, img.width); 322 | } 323 | 324 | handler.img && handler.img(img); 325 | }; 326 | 327 | var parseBlock = function() { 328 | var block = {}; 329 | block.sentinel = st.readByte(); 330 | 331 | switch (String.fromCharCode(block.sentinel)) { // For ease of matching 332 | case '!': 333 | block.type = 'ext'; 334 | parseExt(block); 335 | break; 336 | case ',': 337 | block.type = 'img'; 338 | parseImg(block); 339 | break; 340 | case ';': 341 | block.type = 'eof'; 342 | handler.eof && handler.eof(block); 343 | break; 344 | default: 345 | throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0. 346 | } 347 | 348 | if (block.type !== 'eof') setTimeout(parseBlock, 0); 349 | }; 350 | 351 | var parse = function() { 352 | parseHeader(); 353 | setTimeout(parseBlock, 0); 354 | }; 355 | 356 | parse(); 357 | }; 358 | 359 | var SuperGif = function(opts) { 360 | var options = { 361 | //viewport position 362 | vp_l: 0, 363 | vp_t: 0, 364 | vp_w: null, 365 | vp_h: null, 366 | //canvas sizes 367 | c_w: null, 368 | c_h: null, 369 | auto_play: true 370 | }; 371 | for (var i in opts) { 372 | options[i] = opts[i]; 373 | } 374 | if (options.vp_w && options.vp_h) options.is_vp = true; 375 | 376 | var stream; 377 | var hdr; 378 | 379 | var loadError = null; 380 | var loading = false; 381 | 382 | var transparency = null; 383 | var delay = null; 384 | var disposalMethod = null; 385 | var disposalRestoreFromIdx = null; 386 | var lastDisposalMethod = null; 387 | var frame = null; 388 | var lastImg = null; 389 | 390 | var playing = true; 391 | var forward = true; 392 | 393 | var ctx_scaled = false; 394 | 395 | var frames = []; 396 | var frameOffsets = []; // elements have .x and .y properties 397 | 398 | var gif = document.createElement('img'); 399 | gif.src = options.gif; 400 | 401 | var onEndListener = (options.hasOwnProperty('on_end') ? options.on_end : null); 402 | var loopDelay = (options.hasOwnProperty('loop_delay') ? options.loop_delay : 0); 403 | var overrideLoopMode = (options.hasOwnProperty('loop_mode') ? options.loop_mode : 'auto'); 404 | var drawWhileLoading = false; 405 | 406 | var clear = function() { 407 | transparency = null; 408 | delay = null; 409 | lastDisposalMethod = disposalMethod; 410 | disposalMethod = null; 411 | frame = null; 412 | }; 413 | 414 | // XXX: There's probably a better way to handle catching exceptions when 415 | // callbacks are involved. 416 | var doParse = function() { 417 | try { 418 | parseGIF(stream, handler); 419 | } catch (err) { 420 | doLoadError('parse'); 421 | } 422 | }; 423 | 424 | var setSizes = function(w, h) { 425 | pGraphics.resize(w, h); 426 | pGraphics.width = w; 427 | pGraphics.height = h; 428 | }; 429 | 430 | var setFrameOffset = function(frame, offset) { 431 | if (!frameOffsets[frame]) { 432 | frameOffsets[frame] = offset; 433 | return; 434 | } 435 | if (typeof offset.x !== 'undefined') { 436 | frameOffsets[frame].x = offset.x; 437 | } 438 | if (typeof offset.y !== 'undefined') { 439 | frameOffsets[frame].y = offset.y; 440 | } 441 | }; 442 | 443 | var doLoadError = function(originOfError) { 444 | loadError = originOfError; 445 | frames = []; 446 | }; 447 | 448 | var doHdr = function(_hdr) { 449 | hdr = _hdr; 450 | setSizes(hdr.width, hdr.height); 451 | }; 452 | 453 | var doGCE = function(gce) { 454 | pushFrame(); 455 | clear(); 456 | transparency = gce.transparencyGiven ? gce.transparencyIndex : null; 457 | delay = gce.delayTime; 458 | disposalMethod = gce.disposalMethod; 459 | // We don't have much to do with the rest of GCE. 460 | }; 461 | 462 | var pushFrame = function() { 463 | if (!frame) return; 464 | frames.push({ 465 | data: frame.getImageData(0, 0, hdr.width, hdr.height), 466 | delay: delay 467 | }); 468 | frameOffsets.push({ 469 | x: 0, 470 | y: 0 471 | }); 472 | }; 473 | 474 | var doImg = function(img) { 475 | if (!frame) frame = tmpCanvas.getContext('2d'); 476 | 477 | var currIdx = frames.length; 478 | 479 | //ct = color table, gct = global color table 480 | var ct = img.lctFlag ? img.lct : hdr.gct; // TODO: What if neither exists? 481 | 482 | /* 483 | Disposal method indicates the way in which the graphic is to 484 | be treated after being displayed. 485 | 486 | Values : 0 - No disposal specified. The decoder is 487 | not required to take any action. 488 | 1 - Do not dispose. The graphic is to be left 489 | in place. 490 | 2 - Restore to background color. The area used by the 491 | graphic must be restored to the background color. 492 | 3 - Restore to previous. The decoder is required to 493 | restore the area overwritten by the graphic with 494 | what was there prior to rendering the graphic. 495 | 496 | Importantly, "previous" means the frame state 497 | after the last disposal of method 0, 1, or 2. 498 | */ 499 | if (currIdx > 0) { 500 | if (lastDisposalMethod === 3) { 501 | // Restore to previous 502 | // If we disposed every frame including first frame up to this point, then we have 503 | // no composited frame to restore to. In this case, restore to background instead. 504 | if (disposalRestoreFromIdx !== null) { 505 | frame.putImageData(frames[disposalRestoreFromIdx].data, 0, 0); 506 | } else { 507 | frame.clearRect(lastImg.leftPos, lastImg.topPos, lastImg.width, lastImg.height); 508 | } 509 | } else { 510 | disposalRestoreFromIdx = currIdx - 1; 511 | } 512 | 513 | if (lastDisposalMethod === 2) { 514 | // Restore to background color 515 | // Browser implementations historically restore to transparent; we do the same. 516 | // http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079 517 | frame.clearRect(lastImg.leftPos, lastImg.topPos, lastImg.width, lastImg.height); 518 | } 519 | } 520 | // else, Undefined/Do not dispose. 521 | // frame contains final pixel data from the last frame; do nothing 522 | 523 | //Get existing pixels for img region after applying disposal method 524 | var imgData = frame.getImageData(img.leftPos, img.topPos, img.width, img.height); 525 | 526 | //apply color table colors 527 | var cdd = imgData.data; 528 | img.pixels.forEach(function(pixel, i) { 529 | // imgData.data === [R,G,B,A,R,G,B,A,...] 530 | if (pixel !== transparency) { 531 | cdd[i * 4 + 0] = ct[pixel][0]; 532 | cdd[i * 4 + 1] = ct[pixel][1]; 533 | cdd[i * 4 + 2] = ct[pixel][2]; 534 | cdd[i * 4 + 3] = 255; // Opaque. 535 | } 536 | }); 537 | imgData.data.set(cdd); 538 | 539 | frame.putImageData(imgData, img.leftPos, img.topPos); 540 | 541 | if (!ctx_scaled) { 542 | ctx_scaled = true; 543 | } 544 | 545 | lastImg = img; 546 | }; 547 | 548 | var player = (function() { 549 | var i = -1; 550 | var iterationCount = 0; 551 | 552 | var showingInfo = false; 553 | var pinned = false; 554 | 555 | /** 556 | * Gets the index of the frame "up next". 557 | * @returns {number} 558 | */ 559 | var getNextFrameNo = function() { 560 | var delta = (forward ? 1 : -1); 561 | return (i + delta + frames.length) % frames.length; 562 | }; 563 | 564 | var stepFrame = function(amount) { // XXX: Name is confusing. 565 | i = i + amount; 566 | 567 | putFrame(); 568 | }; 569 | 570 | var completeLoop = function() { 571 | if (onEndListener !== null) 572 | onEndListener(gif); 573 | iterationCount++; 574 | }; 575 | 576 | var step = (function() { 577 | var stepping = false; 578 | 579 | var doStep = function() { 580 | stepping = playing; 581 | if (!stepping) return; 582 | 583 | stepFrame(1); 584 | var delay = frames[i].delay * 10; 585 | if (!delay) delay = 100; // FIXME: Should this even default at all? What should it be? 586 | 587 | var nextFrameNo = getNextFrameNo(); 588 | if (nextFrameNo === 0) { 589 | delay += loopDelay; 590 | setTimeout(completeLoop, delay - 1); 591 | } 592 | 593 | if ((overrideLoopMode !== false || nextFrameNo !== 0 || iterationCount < 0)) 594 | setTimeout(doStep, delay); 595 | 596 | }; 597 | 598 | return function() { 599 | if (!stepping) setTimeout(doStep, 0); 600 | }; 601 | }()); 602 | 603 | var putFrame = function() { 604 | var offset; 605 | i = parseInt(i, 10); 606 | 607 | if (i > frames.length - 1) { 608 | i = 0; 609 | } 610 | 611 | if (i < 0) { 612 | i = 0; 613 | } 614 | 615 | offset = frameOffsets[i]; 616 | 617 | tmpCanvas.getContext("2d").putImageData(frames[i].data, offset.x, offset.y); 618 | }; 619 | 620 | var play = function() { 621 | playing = true; 622 | step(); 623 | }; 624 | 625 | var pause = function() { 626 | playing = false; 627 | }; 628 | 629 | 630 | 631 | return { 632 | init: function() { 633 | if (loadError) return; 634 | 635 | if (options.auto_play) { 636 | step(); 637 | } else { 638 | i = 0; 639 | putFrame(); 640 | } 641 | }, 642 | step: step, 643 | play: play, 644 | pause: pause, 645 | playing: playing, 646 | move_relative: stepFrame, 647 | current_frame: function() { 648 | return i; 649 | }, 650 | length: function() { 651 | return frames.length; 652 | }, 653 | move_to: function(frame_idx) { 654 | i = frame_idx; 655 | putFrame(); 656 | }, 657 | get_frames: function() { 658 | return frames; 659 | }, 660 | buffer: function() { 661 | return pGraphics; 662 | }, 663 | get_playing: function() { 664 | return playing; 665 | }, 666 | }; 667 | }()); 668 | 669 | var doNothing = function() {}; 670 | /** 671 | * @param{boolean=} draw Whether to draw progress bar or not; this is not idempotent because of translucency. 672 | * Note that this means that the text will be unsynchronized with the progress bar on non-frames; 673 | * but those are typically so small (GCE etc.) that it doesn't really matter. TODO: Do this properly. 674 | */ 675 | var withProgress = function(fn, draw) { 676 | return function(block) { 677 | fn(block); 678 | }; 679 | }; 680 | 681 | 682 | var handler = { 683 | hdr: withProgress(doHdr), 684 | gce: withProgress(doGCE), 685 | com: withProgress(doNothing), 686 | // I guess that's all for now. 687 | app: { 688 | // TODO: Is there much point in actually supporting iterations? 689 | NETSCAPE: withProgress(doNothing) 690 | }, 691 | img: withProgress(doImg, true), 692 | eof: function(block) { 693 | pushFrame(); 694 | player.init(); 695 | loading = false; 696 | console.log('ok'); 697 | if (load_callback) { 698 | load_callback(gif); 699 | } 700 | 701 | } 702 | }; 703 | 704 | var init = function() { 705 | pGraphics = options.p5inst.createImage(0, 0); 706 | tmpCanvas = pGraphics.canvas; 707 | 708 | initialized = true; 709 | }; 710 | 711 | var get_canvas_scale = function() { 712 | var scale; 713 | if (options.max_width && hdr && hdr.width > options.max_width) { 714 | scale = options.max_width / hdr.width; 715 | } else { 716 | scale = 1; 717 | } 718 | return scale; 719 | }; 720 | 721 | var tmpCanvas, pGraphics; 722 | var initialized = false; 723 | var load_callback = false; 724 | 725 | var load_setup = function(callback) { 726 | if (loading) return false; 727 | if (callback) load_callback = callback; 728 | else load_callback = false; 729 | 730 | loading = true; 731 | frames = []; 732 | clear(); 733 | disposalRestoreFromIdx = null; 734 | lastDisposalMethod = null; 735 | frame = null; 736 | lastImg = null; 737 | 738 | return true; 739 | }; 740 | 741 | return { 742 | // play controls 743 | play: player.play, 744 | pause: player.pause, 745 | move_relative: player.move_relative, 746 | move_to: player.move_to, 747 | 748 | // getters for instance vars 749 | get_playing: player.get_playing, 750 | get_canvas: function() { 751 | return canvas; 752 | }, 753 | get_canvas_scale: function() { 754 | return get_canvas_scale(); 755 | }, 756 | get_loading: function() { 757 | return loading; 758 | }, 759 | get_auto_play: function() { 760 | return options.auto_play; 761 | }, 762 | get_length: function() { 763 | return player.length(); 764 | }, 765 | get_current_frame: function() { 766 | return player.current_frame(); 767 | }, 768 | get_frames: function() { 769 | return player.get_frames(); 770 | }, 771 | buffer: function() { 772 | return player.buffer(); 773 | }, 774 | load_url: function(src, callback) { 775 | if (!load_setup(callback)) return; 776 | var h = new XMLHttpRequest(); 777 | h.overrideMimeType('text/plain; charset=x-user-defined'); 778 | h.onloadstart = function() { 779 | // Wait until connection is opened to replace the gif element with a canvas to avoid a blank img 780 | if (!initialized) init(); 781 | }; 782 | h.onload = function(e) { 783 | stream = new Stream(h.responseText); 784 | setTimeout(doParse, 0); 785 | }; 786 | h.onerror = function() { 787 | doLoadError('xhr'); 788 | }; 789 | h.open('GET', src, true); 790 | h.send(); 791 | }, 792 | load: function(callback) { 793 | this.load_url(gif.src, callback); 794 | }, 795 | load_raw: function(arr, callback) { 796 | if (!load_setup(callback)) return; 797 | if (!initialized) init(); 798 | stream = new Stream(arr); 799 | setTimeout(doParse, 0); 800 | }, 801 | set_frame_offset: setFrameOffset 802 | }; 803 | }; 804 | 805 | return SuperGif; 806 | })); 807 | 808 | 809 | (function() { 810 | 811 | p5.prototype.loadGif = function(url, cb) { 812 | var gif = new SuperGif({ 813 | gif: url, 814 | p5inst: this 815 | }); 816 | 817 | gif.load(cb); 818 | 819 | var p5graphic = gif.buffer(); 820 | 821 | p5graphic.play = gif.play; 822 | p5graphic.pause = gif.pause; 823 | p5graphic.playing = gif.get_playing; 824 | p5graphic.frames = gif.get_frames; 825 | p5graphic.totalFrames = gif.get_length; 826 | 827 | p5graphic.loaded = function() { 828 | return !gif.get_loading(); 829 | }; 830 | 831 | p5graphic.frame = function(num) { 832 | if (typeof num === 'number') { 833 | gif.move_to(num); 834 | } else { 835 | return gif.get_current_frame(); 836 | } 837 | }; 838 | 839 | return p5graphic; 840 | }; 841 | 842 | p5.prototype.loadRawGif = function(data, cb) { 843 | var gif = new SuperGif({ 844 | gif: '', 845 | p5inst: this 846 | }); 847 | 848 | gif.load_raw(data, cb); 849 | 850 | var p5graphic = gif.buffer(); 851 | p5graphic.play = gif.play; 852 | p5graphic.pause = gif.pause; 853 | p5graphic.playing = gif.get_playing; 854 | p5graphic.frames = gif.get_frames; 855 | p5graphic.totalFrames = gif.get_length; 856 | 857 | p5graphic.loaded = function() { 858 | return !gif.get_loading(); 859 | }; 860 | 861 | p5graphic.frame = function(num) { 862 | if (typeof num === 'number') { 863 | gif.move_to(num); 864 | } else { 865 | return gif.get_current_frame(); 866 | } 867 | }; 868 | 869 | return p5graphic; 870 | }; 871 | 872 | })(); 873 | -------------------------------------------------------------------------------- /libs/workshop-utils.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | 4 | Global variables to make our life easier 5 | 6 | */ 7 | 8 | var ctracker; 9 | var videoInput; 10 | var positions = []; 11 | var canvas; 12 | 13 | /* 14 | 15 | Load & align the canvas to match the underlying html5 video element 16 | 17 | */ 18 | 19 | function loadCanvas(w, h) { 20 | canvas = createCanvas(w, h); 21 | canvas.position(0,0); 22 | } 23 | 24 | /* 25 | 26 | Load the capture device, align under the canvas & mute it to avoid audiofeedback 27 | 28 | */ 29 | 30 | function loadCamera() { 31 | // setup camera capture 32 | videoInput = createCapture(VIDEO); 33 | videoInput.size(400, 300); 34 | videoInput.position(0, 0); 35 | videoInput.id("v"); 36 | var mv = document.getElementById("v"); 37 | mv.muted = true; 38 | } 39 | 40 | /* 41 | 42 | Load ClmTracker, apply to the video element 43 | 44 | */ 45 | 46 | function loadTracker() { 47 | // setup tracker 48 | ctracker = new clm.tracker(); 49 | ctracker.init(pModel); 50 | ctracker.start(videoInput.elt); 51 | } 52 | 53 | /* 54 | 55 | Get current face feature point positions. Should go into the draw() function 56 | 57 | */ 58 | 59 | function getPositions() { 60 | // get array of face marker positions [x, y] format 61 | positions = ctracker.getCurrentPosition(); 62 | } 63 | 64 | 65 | /* 66 | 67 | Get current emotion predictions. Should go into the draw() function 68 | 69 | */ 70 | 71 | function getEmotions() { 72 | var cp = ctracker.getCurrentParameters(); 73 | predictedEmotions = emotions.meanPredict(cp); 74 | } 75 | 76 | /* 77 | 78 | Emotion-specific global variables 79 | 80 | */ 81 | 82 | delete emotionModel['disgusted']; 83 | delete emotionModel['fear']; 84 | var emotions = new emotionClassifier(); 85 | var predictedEmotions; 86 | emotions.init(emotionModel); 87 | var emotionData = emotions.getBlank(); 88 | 89 | --------------------------------------------------------------------------------