From 72ed09460315afabd4914a170380b04cb36cc854 Mon Sep 17 00:00:00 2001 From: 12913378 <harry.buttenshaw@student.uts.edu.au> Date: Sun, 27 Oct 2019 19:11:33 +1100 Subject: [PATCH] Upload New File --- Code/p5.dom.js | 3400 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3400 insertions(+) create mode 100644 Code/p5.dom.js diff --git a/Code/p5.dom.js b/Code/p5.dom.js new file mode 100644 index 0000000..2bcb9f5 --- /dev/null +++ b/Code/p5.dom.js @@ -0,0 +1,3400 @@ +/*! p5.js v0.9.0 July 01, 2019 */ +/** + * <p>The web is much more than just canvas and p5.dom makes it easy to interact + * with other HTML5 objects, including text, hyperlink, image, input, video, + * audio, and webcam.</p> + * <p>There is a set of creation methods, DOM manipulation methods, and + * an extended <a href="#/p5.Element">p5.Element</a> that supports a range of HTML elements. See the + * <a href='https://github.com/processing/p5.js/wiki/Beyond-the-canvas'> + * beyond the canvas tutorial</a> for a full overview of how this addon works. + * + * <p>Methods and properties shown in black are part of the p5.js core, items in + * blue are part of the p5.dom library. You will need to include an extra file + * in order to access the blue functions. See the + * <a href='http://p5js.org/libraries/#using-a-library'>using a library</a> + * section for information on how to include this library. p5.dom comes with + * <a href='http://p5js.org/download'>p5 complete</a> or you can download the single file + * <a href='https://raw.githubusercontent.com/lmccart/p5.js/master/lib/addons/p5.dom.js'> + * here</a>.</p> + * <p>See <a href='https://github.com/processing/p5.js/wiki/Beyond-the-canvas'>tutorial: beyond the canvas</a> + * for more info on how to use this library.</a> + * + * @module p5.dom + * @submodule p5.dom + * @for p5 + * @main + */ + +(function(root, factory) { + if (typeof define === 'function' && define.amd) + define('p5.dom', ['p5'], function(p5) { + factory(p5); + }); + else if (typeof exports === 'object') factory(require('../p5')); + else factory(root['p5']); +})(this, function(p5) { + // ============================================================================= + // p5 additions + // ============================================================================= + + /** + * Searches the page for an element with the given ID, class, or tag name (using the '#' or '.' + * prefixes to specify an ID or class respectively, and none for a tag) and returns it as + * a <a href="#/p5.Element">p5.Element</a>. If a class or tag name is given with more than 1 element, + * only the first element will be returned. + * The DOM node itself can be accessed with .elt. + * Returns null if none found. You can also specify a container to search within. + * + * @method select + * @param {String} name id, class, or tag name of element to search for + * @param {String|p5.Element|HTMLElement} [container] id, <a href="#/p5.Element">p5.Element</a>, or + * HTML element to search within + * @return {p5.Element|null} <a href="#/p5.Element">p5.Element</a> containing node found + * @example + * <div ><code class='norender'> + * function setup() { + * createCanvas(100, 100); + * //translates canvas 50px down + * select('canvas').position(100, 100); + * } + * </code></div> + * <div><code class='norender'> + * // these are all valid calls to select() + * var a = select('#moo'); + * var b = select('#blah', '#myContainer'); + * var c, e; + * if (b) { + * c = select('#foo', b); + * } + * var d = document.getElementById('beep'); + * if (d) { + * e = select('p', d); + * } + * [a, b, c, d, e]; // unused + * </code></div> + * + */ + p5.prototype.select = function(e, p) { + p5._validateParameters('select', arguments); + var res = null; + var container = getContainer(p); + if (e[0] === '.') { + e = e.slice(1); + res = container.getElementsByClassName(e); + if (res.length) { + res = res[0]; + } else { + res = null; + } + } else if (e[0] === '#') { + e = e.slice(1); + res = container.getElementById(e); + } else { + res = container.getElementsByTagName(e); + if (res.length) { + res = res[0]; + } else { + res = null; + } + } + if (res) { + return this._wrapElement(res); + } else { + return null; + } + }; + + /** + * Searches the page for elements with the given class or tag name (using the '.' prefix + * to specify a class and no prefix for a tag) and returns them as <a href="#/p5.Element">p5.Element</a>s + * in an array. + * The DOM node itself can be accessed with .elt. + * Returns an empty array if none found. + * You can also specify a container to search within. + * + * @method selectAll + * @param {String} name class or tag name of elements to search for + * @param {String} [container] id, <a href="#/p5.Element">p5.Element</a>, or HTML element to search within + * @return {p5.Element[]} Array of <a href="#/p5.Element">p5.Element</a>s containing nodes found + * @example + * <div class='norender'><code> + * function setup() { + * createButton('btn'); + * createButton('2nd btn'); + * createButton('3rd btn'); + * var buttons = selectAll('button'); + * + * for (var i = 0; i < buttons.length; i++) { + * buttons[i].size(100, 100); + * } + * } + * </code></div> + * <div class='norender'><code> + * // these are all valid calls to selectAll() + * var a = selectAll('.moo'); + * a = selectAll('div'); + * a = selectAll('button', '#myContainer'); + * + * var d = select('#container'); + * a = selectAll('p', d); + * + * var f = document.getElementById('beep'); + * a = select('.blah', f); + * + * a; // unused + * </code></div> + * + */ + p5.prototype.selectAll = function(e, p) { + p5._validateParameters('selectAll', arguments); + var arr = []; + var res; + var container = getContainer(p); + if (e[0] === '.') { + e = e.slice(1); + res = container.getElementsByClassName(e); + } else { + res = container.getElementsByTagName(e); + } + if (res) { + for (var j = 0; j < res.length; j++) { + var obj = this._wrapElement(res[j]); + arr.push(obj); + } + } + return arr; + }; + + /** + * Helper function for select and selectAll + */ + function getContainer(p) { + var container = document; + if (typeof p === 'string' && p[0] === '#') { + p = p.slice(1); + container = document.getElementById(p) || document; + } else if (p instanceof p5.Element) { + container = p.elt; + } else if (p instanceof HTMLElement) { + container = p; + } + return container; + } + + /** + * Helper function for getElement and getElements. + */ + p5.prototype._wrapElement = function(elt) { + var children = Array.prototype.slice.call(elt.children); + if (elt.tagName === 'INPUT' && elt.type === 'checkbox') { + var converted = new p5.Element(elt, this); + converted.checked = function() { + if (arguments.length === 0) { + return this.elt.checked; + } else if (arguments[0]) { + this.elt.checked = true; + } else { + this.elt.checked = false; + } + return this; + }; + return converted; + } else if (elt.tagName === 'VIDEO' || elt.tagName === 'AUDIO') { + return new p5.MediaElement(elt, this); + } else if (elt.tagName === 'SELECT') { + return this.createSelect(new p5.Element(elt, this)); + } else if ( + children.length > 0 && + children.every(function(c) { + return c.tagName === 'INPUT' || c.tagName === 'LABEL'; + }) + ) { + return this.createRadio(new p5.Element(elt, this)); + } else { + return new p5.Element(elt, this); + } + }; + + /** + * Removes all elements created by p5, except any canvas / graphics + * elements created by <a href="#/p5/createCanvas">createCanvas</a> or <a href="#/p5/createGraphics">createGraphics</a>. + * Event handlers are removed, and element is removed from the DOM. + * @method removeElements + * @example + * <div class='norender'><code> + * function setup() { + * createCanvas(100, 100); + * createDiv('this is some text'); + * createP('this is a paragraph'); + * } + * function mousePressed() { + * removeElements(); // this will remove the div and p, not canvas + * } + * </code></div> + * + */ + p5.prototype.removeElements = function(e) { + p5._validateParameters('removeElements', arguments); + for (var i = 0; i < this._elements.length; i++) { + if (!(this._elements[i].elt instanceof HTMLCanvasElement)) { + this._elements[i].remove(); + } + } + }; + + /** + * The .<a href="#/p5.Element/changed">changed()</a> function is called when the value of an + * element changes. + * This can be used to attach an element specific event listener. + * + * @method changed + * @param {Function|Boolean} fxn function to be fired when the value of + * an element changes. + * if `false` is passed instead, the previously + * firing function will no longer fire. + * @chainable + * @example + * <div><code> + * var sel; + * + * function setup() { + * textAlign(CENTER); + * background(200); + * sel = createSelect(); + * sel.position(10, 10); + * sel.option('pear'); + * sel.option('kiwi'); + * sel.option('grape'); + * sel.changed(mySelectEvent); + * } + * + * function mySelectEvent() { + * var item = sel.value(); + * background(200); + * text("it's a " + item + '!', 50, 50); + * } + * </code></div> + * + * <div><code> + * var checkbox; + * var cnv; + * + * function setup() { + * checkbox = createCheckbox(' fill'); + * checkbox.changed(changeFill); + * cnv = createCanvas(100, 100); + * cnv.position(0, 30); + * noFill(); + * } + * + * function draw() { + * background(200); + * ellipse(50, 50, 50, 50); + * } + * + * function changeFill() { + * if (checkbox.checked()) { + * fill(0); + * } else { + * noFill(); + * } + * } + * </code></div> + * + * @alt + * dropdown: pear, kiwi, grape. When selected text "its a" + selection shown. + * + */ + p5.Element.prototype.changed = function(fxn) { + p5.Element._adjustListener('change', fxn, this); + return this; + }; + + /** + * The .<a href="#/p5.Element/input">input()</a> function is called when any user input is + * detected with an element. The input event is often used + * to detect keystrokes in a input element, or changes on a + * slider element. This can be used to attach an element specific + * event listener. + * + * @method input + * @param {Function|Boolean} fxn function to be fired when any user input is + * detected within the element. + * if `false` is passed instead, the previously + * firing function will no longer fire. + * @chainable + * @example + * <div class='norender'><code> + * // Open your console to see the output + * function setup() { + * var inp = createInput(''); + * inp.input(myInputEvent); + * } + * + * function myInputEvent() { + * console.log('you are typing: ', this.value()); + * } + * </code></div> + * + * @alt + * no display. + * + */ + p5.Element.prototype.input = function(fxn) { + p5.Element._adjustListener('input', fxn, this); + return this; + }; + + /** + * Helpers for create methods. + */ + function addElement(elt, pInst, media) { + var node = pInst._userNode ? pInst._userNode : document.body; + node.appendChild(elt); + var c = media + ? new p5.MediaElement(elt, pInst) + : new p5.Element(elt, pInst); + pInst._elements.push(c); + return c; + } + + /** + * Creates a <div></div> element in the DOM with given inner HTML. + * Appends to the container node if one is specified, otherwise + * appends to body. + * + * @method createDiv + * @param {String} [html] inner HTML for element created + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div class='norender'><code> + * createDiv('this is some text'); + * </code></div> + */ + + /** + * Creates a <p></p> element in the DOM with given inner HTML. Used + * for paragraph length text. + * Appends to the container node if one is specified, otherwise + * appends to body. + * + * @method createP + * @param {String} [html] inner HTML for element created + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div class='norender'><code> + * createP('this is some text'); + * </code></div> + */ + + /** + * Creates a <span></span> element in the DOM with given inner HTML. + * Appends to the container node if one is specified, otherwise + * appends to body. + * + * @method createSpan + * @param {String} [html] inner HTML for element created + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div class='norender'><code> + * createSpan('this is some text'); + * </code></div> + */ + var tags = ['div', 'p', 'span']; + tags.forEach(function(tag) { + var method = 'create' + tag.charAt(0).toUpperCase() + tag.slice(1); + p5.prototype[method] = function(html) { + var elt = document.createElement(tag); + elt.innerHTML = typeof html === 'undefined' ? '' : html; + return addElement(elt, this); + }; + }); + + /** + * Creates an <img> element in the DOM with given src and + * alternate text. + * Appends to the container node if one is specified, otherwise + * appends to body. + * + * @method createImg + * @param {String} src src path or url for image + * @param {String} [alt] alternate text to be used if image does not load + * @param {Function} [successCallback] callback to be called once image data is loaded + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div class='norender'><code> + * createImg('http://p5js.org/img/asterisk-01.png'); + * </code></div> + */ + /** + * @method createImg + * @param {String} src + * @param {Function} successCallback + * @return {Object|p5.Element} + */ + p5.prototype.createImg = function() { + p5._validateParameters('createImg', arguments); + var elt = document.createElement('img'); + elt.crossOrigin = 'Anonymous'; + var args = arguments; + var self; + var setAttrs = function() { + self.width = elt.offsetWidth || elt.width; + self.height = elt.offsetHeight || elt.height; + if (args.length > 1 && typeof args[1] === 'function') { + self.fn = args[1]; + self.fn(); + } else if (args.length > 1 && typeof args[2] === 'function') { + self.fn = args[2]; + self.fn(); + } + }; + elt.src = args[0]; + if (args.length > 1 && typeof args[1] === 'string') { + elt.alt = args[1]; + } + elt.onload = function() { + setAttrs(); + }; + self = addElement(elt, this); + return self; + }; + + /** + * Creates an <a></a> element in the DOM for including a hyperlink. + * Appends to the container node if one is specified, otherwise + * appends to body. + * + * @method createA + * @param {String} href url of page to link to + * @param {String} html inner html of link element to display + * @param {String} [target] target where new link should open, + * could be _blank, _self, _parent, _top. + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div class='norender'><code> + * createA('http://p5js.org/', 'this is a link'); + * </code></div> + */ + p5.prototype.createA = function(href, html, target) { + p5._validateParameters('createA', arguments); + var elt = document.createElement('a'); + elt.href = href; + elt.innerHTML = html; + if (target) elt.target = target; + return addElement(elt, this); + }; + + /** INPUT **/ + + /** + * Creates a slider <input></input> element in the DOM. + * Use .size() to set the display length of the slider. + * Appends to the container node if one is specified, otherwise + * appends to body. + * + * @method createSlider + * @param {Number} min minimum value of the slider + * @param {Number} max maximum value of the slider + * @param {Number} [value] default value of the slider + * @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) + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div><code> + * var slider; + * function setup() { + * slider = createSlider(0, 255, 100); + * slider.position(10, 10); + * slider.style('width', '80px'); + * } + * + * function draw() { + * var val = slider.value(); + * background(val); + * } + * </code></div> + * + * <div><code> + * var slider; + * function setup() { + * colorMode(HSB); + * slider = createSlider(0, 360, 60, 40); + * slider.position(10, 10); + * slider.style('width', '80px'); + * } + * + * function draw() { + * var val = slider.value(); + * background(val, 100, 100, 1); + * } + * </code></div> + */ + p5.prototype.createSlider = function(min, max, value, step) { + p5._validateParameters('createSlider', arguments); + var elt = document.createElement('input'); + elt.type = 'range'; + elt.min = min; + elt.max = max; + if (step === 0) { + elt.step = 0.000000000000000001; // smallest valid step + } else if (step) { + elt.step = step; + } + if (typeof value === 'number') elt.value = value; + return addElement(elt, this); + }; + + /** + * Creates a <button></button> element in the DOM. + * Use .size() to set the display size of the button. + * Use .mousePressed() to specify behavior on press. + * Appends to the container node if one is specified, otherwise + * appends to body. + * + * @method createButton + * @param {String} label label displayed on the button + * @param {String} [value] value of the button + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div class='norender'><code> + * var button; + * function setup() { + * createCanvas(100, 100); + * background(0); + * button = createButton('click me'); + * button.position(19, 19); + * button.mousePressed(changeBG); + * } + * + * function changeBG() { + * var val = random(255); + * background(val); + * } + * </code></div> + */ + p5.prototype.createButton = function(label, value) { + p5._validateParameters('createButton', arguments); + var elt = document.createElement('button'); + elt.innerHTML = label; + if (value) elt.value = value; + return addElement(elt, this); + }; + + /** + * Creates a checkbox <input></input> element in the DOM. + * Calling .checked() on a checkbox returns if it is checked or not + * + * @method createCheckbox + * @param {String} [label] label displayed after checkbox + * @param {boolean} [value] value of the checkbox; checked is true, unchecked is false + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div class='norender'><code> + * var checkbox; + * + * function setup() { + * checkbox = createCheckbox('label', false); + * checkbox.changed(myCheckedEvent); + * } + * + * function myCheckedEvent() { + * if (this.checked()) { + * console.log('Checking!'); + * } else { + * console.log('Unchecking!'); + * } + * } + * </code></div> + */ + p5.prototype.createCheckbox = function() { + p5._validateParameters('createCheckbox', arguments); + var elt = document.createElement('div'); + var checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + elt.appendChild(checkbox); + //checkbox must be wrapped in p5.Element before label so that label appears after + var self = addElement(elt, this); + self.checked = function() { + var cb = self.elt.getElementsByTagName('input')[0]; + if (cb) { + if (arguments.length === 0) { + return cb.checked; + } else if (arguments[0]) { + cb.checked = true; + } else { + cb.checked = false; + } + } + return self; + }; + this.value = function(val) { + self.value = val; + return this; + }; + if (arguments[0]) { + var ran = Math.random() + .toString(36) + .slice(2); + var label = document.createElement('label'); + checkbox.setAttribute('id', ran); + label.htmlFor = ran; + self.value(arguments[0]); + label.appendChild(document.createTextNode(arguments[0])); + elt.appendChild(label); + } + if (arguments[1]) { + checkbox.checked = true; + } + return self; + }; + + /** + * Creates a dropdown menu <select></select> element in the DOM. + * It also helps to assign select-box methods to <a href="#/p5.Element">p5.Element</a> when selecting existing select box + * @method createSelect + * @param {boolean} [multiple] true if dropdown should support multiple selections + * @return {p5.Element} + * @example + * <div><code> + * var sel; + * + * function setup() { + * textAlign(CENTER); + * background(200); + * sel = createSelect(); + * sel.position(10, 10); + * sel.option('pear'); + * sel.option('kiwi'); + * sel.option('grape'); + * sel.changed(mySelectEvent); + * } + * + * function mySelectEvent() { + * var item = sel.value(); + * background(200); + * text('It is a ' + item + '!', 50, 50); + * } + * </code></div> + */ + /** + * @method createSelect + * @param {Object} existing DOM select element + * @return {p5.Element} + */ + + p5.prototype.createSelect = function() { + p5._validateParameters('createSelect', arguments); + var elt, self; + var arg = arguments[0]; + if (typeof arg === 'object' && arg.elt.nodeName === 'SELECT') { + self = arg; + elt = this.elt = arg.elt; + } else { + elt = document.createElement('select'); + if (arg && typeof arg === 'boolean') { + elt.setAttribute('multiple', 'true'); + } + self = addElement(elt, this); + } + self.option = function(name, value) { + var index; + //see if there is already an option with this name + for (var i = 0; i < this.elt.length; i++) { + if (this.elt[i].innerHTML === name) { + index = i; + break; + } + } + //if there is an option with this name we will modify it + if (index !== undefined) { + //if the user passed in false then delete that option + if (value === false) { + this.elt.remove(index); + } else { + //otherwise if the name and value are the same then change both + if (this.elt[index].innerHTML === this.elt[index].value) { + this.elt[index].innerHTML = this.elt[index].value = value; + //otherwise just change the value + } else { + this.elt[index].value = value; + } + } + } else { + //if it doesn't exist make it + var opt = document.createElement('option'); + opt.innerHTML = name; + if (arguments.length > 1) opt.value = value; + else opt.value = name; + elt.appendChild(opt); + } + }; + self.selected = function(value) { + var arr = [], + i; + if (arguments.length > 0) { + for (i = 0; i < this.elt.length; i++) { + if (value.toString() === this.elt[i].value) { + this.elt.selectedIndex = i; + } + } + return this; + } else { + if (this.elt.getAttribute('multiple')) { + for (i = 0; i < this.elt.selectedOptions.length; i++) { + arr.push(this.elt.selectedOptions[i].value); + } + return arr; + } else { + return this.elt.value; + } + } + }; + return self; + }; + + /** + * Creates a radio button <input></input> element in the DOM. + * The .option() method can be used to set options for the radio after it is + * created. The .value() method will return the currently selected option. + * + * @method createRadio + * @param {String} [divId] the id and name of the created div and input field respectively + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div><code> + * var radio; + * + * function setup() { + * radio = createRadio(); + * radio.option('black'); + * radio.option('white'); + * radio.option('gray'); + * radio.style('width', '60px'); + * textAlign(CENTER); + * fill(255, 0, 0); + * } + * + * function draw() { + * var val = radio.value(); + * background(val); + * text(val, width / 2, height / 2); + * } + * </code></div> + * <div><code> + * var radio; + * + * function setup() { + * radio = createRadio(); + * radio.option('apple', 1); + * radio.option('bread', 2); + * radio.option('juice', 3); + * radio.style('width', '60px'); + * textAlign(CENTER); + * } + * + * function draw() { + * background(200); + * var val = radio.value(); + * if (val) { + * text('item cost is $' + val, width / 2, height / 2); + * } + * } + * </code></div> + */ + p5.prototype.createRadio = function(existing_radios) { + p5._validateParameters('createRadio', arguments); + // do some prep by counting number of radios on page + var radios = document.querySelectorAll('input[type=radio]'); + var count = 0; + if (radios.length > 1) { + var length = radios.length; + var prev = radios[0].name; + var current = radios[1].name; + count = 1; + for (var i = 1; i < length; i++) { + current = radios[i].name; + if (prev !== current) { + count++; + } + prev = current; + } + } else if (radios.length === 1) { + count = 1; + } + // see if we got an existing set of radios from callee + var elt, self; + if (typeof existing_radios === 'object') { + // use existing elements + self = existing_radios; + elt = this.elt = existing_radios.elt; + } else { + // create a set of radio buttons + elt = document.createElement('div'); + self = addElement(elt, this); + } + // setup member functions + self._getInputChildrenArray = function() { + return Array.prototype.slice.call(this.elt.children).filter(function(c) { + return c.tagName === 'INPUT'; + }); + }; + + var times = -1; + self.option = function(name, value) { + var opt = document.createElement('input'); + opt.type = 'radio'; + opt.innerHTML = name; + if (value) opt.value = value; + else opt.value = name; + opt.setAttribute('name', 'defaultradio' + count); + elt.appendChild(opt); + if (name) { + times++; + var label = document.createElement('label'); + opt.setAttribute('id', 'defaultradio' + count + '-' + times); + label.htmlFor = 'defaultradio' + count + '-' + times; + label.appendChild(document.createTextNode(name)); + elt.appendChild(label); + } + return opt; + }; + self.selected = function(value) { + var i; + var inputChildren = self._getInputChildrenArray(); + if (value) { + for (i = 0; i < inputChildren.length; i++) { + if (inputChildren[i].value === value) inputChildren[i].checked = true; + } + return this; + } else { + for (i = 0; i < inputChildren.length; i++) { + if (inputChildren[i].checked === true) return inputChildren[i].value; + } + } + }; + self.value = function(value) { + var i; + var inputChildren = self._getInputChildrenArray(); + if (value) { + for (i = 0; i < inputChildren.length; i++) { + if (inputChildren[i].value === value) inputChildren[i].checked = true; + } + return this; + } else { + for (i = 0; i < inputChildren.length; i++) { + if (inputChildren[i].checked === true) return inputChildren[i].value; + } + return ''; + } + }; + return self; + }; + + /** + * Creates a colorPicker element in the DOM for color input. + * The .value() method will return a hex string (#rrggbb) of the color. + * The .color() method will return a p5.Color object with the current chosen color. + * + * @method createColorPicker + * @param {String|p5.Color} [value] default color of element + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div> + * <code> + * var inp1, inp2; + * function setup() { + * createCanvas(100, 100); + * background('grey'); + * inp1 = createColorPicker('#ff0000'); + * inp2 = createColorPicker(color('yellow')); + * inp1.input(setShade1); + * inp2.input(setShade2); + * setMidShade(); + * } + * + * function setMidShade() { + * // Finding a shade between the two + * var commonShade = lerpColor(inp1.color(), inp2.color(), 0.5); + * fill(commonShade); + * rect(20, 20, 60, 60); + * } + * + * function setShade1() { + * setMidShade(); + * console.log('You are choosing shade 1 to be : ', this.value()); + * } + * function setShade2() { + * setMidShade(); + * console.log('You are choosing shade 2 to be : ', this.value()); + * } + * </code> + * </div> + */ + p5.prototype.createColorPicker = function(value) { + p5._validateParameters('createColorPicker', arguments); + var elt = document.createElement('input'); + var self; + elt.type = 'color'; + if (value) { + if (value instanceof p5.Color) { + elt.value = value.toString('#rrggbb'); + } else { + p5.prototype._colorMode = 'rgb'; + p5.prototype._colorMaxes = { + rgb: [255, 255, 255, 255], + hsb: [360, 100, 100, 1], + hsl: [360, 100, 100, 1] + }; + elt.value = p5.prototype.color(value).toString('#rrggbb'); + } + } else { + elt.value = '#000000'; + } + self = addElement(elt, this); + // Method to return a p5.Color object for the given color. + self.color = function() { + if (value.mode) { + p5.prototype._colorMode = value.mode; + } + if (value.maxes) { + p5.prototype._colorMaxes = value.maxes; + } + return p5.prototype.color(this.elt.value); + }; + return self; + }; + + /** + * Creates an <input></input> element in the DOM for text input. + * Use .<a href="#/p5.Element/size">size()</a> to set the display length of the box. + * Appends to the container node if one is specified, otherwise + * appends to body. + * + * @method createInput + * @param {String} [value] default value of the input box + * @param {String} [type] type of text, ie text, password etc. Defaults to text + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div class='norender'><code> + * function setup() { + * var inp = createInput(''); + * inp.input(myInputEvent); + * } + * + * function myInputEvent() { + * console.log('you are typing: ', this.value()); + * } + * </code></div> + */ + p5.prototype.createInput = function(value, type) { + p5._validateParameters('createInput', arguments); + var elt = document.createElement('input'); + elt.type = type ? type : 'text'; + if (value) elt.value = value; + return addElement(elt, this); + }; + + /** + * Creates an <input></input> element in the DOM of type 'file'. + * This allows users to select local files for use in a sketch. + * + * @method createFileInput + * @param {Function} [callback] callback function for when a file loaded + * @param {String} [multiple] optional to allow multiple files selected + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created DOM element + * @example + * <div><code> + * let input; + * let img; + * + * function setup() { + * input = createFileInput(handleFile); + * input.position(0, 0); + * } + * + * function draw() { + * background(255); + * if (img) { + * image(img, 0, 0, width, height); + * } + * } + * + * function handleFile(file) { + * print(file); + * if (file.type === 'image') { + * img = createImg(file.data); + * img.hide(); + * } else { + * img = null; + * } + * } + * </code></div> + */ + p5.prototype.createFileInput = function(callback, multiple) { + p5._validateParameters('createFileInput', arguments); + // Function to handle when a file is selected + // We're simplifying life and assuming that we always + // want to load every selected file + function handleFileSelect(evt) { + // These are the files + var files = evt.target.files; + // Load each one and trigger a callback + for (var i = 0; i < files.length; i++) { + var f = files[i]; + p5.File._load(f, callback); + } + } + // Is the file stuff supported? + if (window.File && window.FileReader && window.FileList && window.Blob) { + // Yup, we're ok and make an input file selector + var elt = document.createElement('input'); + elt.type = 'file'; + + // If we get a second argument that evaluates to true + // then we are looking for multiple files + if (multiple) { + // Anything gets the job done + elt.multiple = 'multiple'; + } + + // Now let's handle when a file was selected + elt.addEventListener('change', handleFileSelect, false); + return addElement(elt, this); + } else { + console.log( + 'The File APIs are not fully supported in this browser. Cannot create element.' + ); + } + }; + + /** VIDEO STUFF **/ + + function createMedia(pInst, type, src, callback) { + var elt = document.createElement(type); + + // allow src to be empty + src = src || ''; + if (typeof src === 'string') { + src = [src]; + } + for (var i = 0; i < src.length; i++) { + var source = document.createElement('source'); + source.src = src[i]; + elt.appendChild(source); + } + if (typeof callback !== 'undefined') { + var callbackHandler = function() { + callback(); + elt.removeEventListener('canplaythrough', callbackHandler); + }; + elt.addEventListener('canplaythrough', callbackHandler); + } + + var c = addElement(elt, pInst, true); + c.loadedmetadata = false; + // set width and height onload metadata + elt.addEventListener('loadedmetadata', function() { + c.width = elt.videoWidth; + c.height = elt.videoHeight; + //c.elt.playbackRate = s; + // set elt width and height if not set + if (c.elt.width === 0) c.elt.width = elt.videoWidth; + if (c.elt.height === 0) c.elt.height = elt.videoHeight; + if (c.presetPlaybackRate) { + c.elt.playbackRate = c.presetPlaybackRate; + delete c.presetPlaybackRate; + } + c.loadedmetadata = true; + }); + + return c; + } + /** + * Creates an HTML5 <video> element in the DOM for simple playback + * of audio/video. Shown by default, can be hidden with .<a href="#/p5.Element/hide">hide()</a> + * and drawn into canvas using video(). Appends to the container + * node if one is specified, otherwise appends to body. The first parameter + * can be either a single string path to a video file, or an array of string + * paths to different formats of the same video. This is useful for ensuring + * that your video can play across different browsers, as each supports + * different formats. See <a href='https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats'>this + * page</a> for further information about supported formats. + * + * @method createVideo + * @param {String|String[]} src path to a video file, or array of paths for + * supporting different browsers + * @param {Function} [callback] callback function to be called upon + * 'canplaythrough' event fire, that is, when the + * browser can play the media, and estimates that + * enough data has been loaded to play the media + * up to its end without having to stop for + * further buffering of content + * @return {p5.MediaElement} pointer to video <a href="#/p5.Element">p5.Element</a> + * @example + * <div><code> + * var vid; + * function setup() { + * noCanvas(); + * + * vid = createVideo( + * ['assets/small.mp4', 'assets/small.ogv', 'assets/small.webm'], + * vidLoad + * ); + * + * vid.size(100, 100); + * } + * + * // This function is called when the video loads + * function vidLoad() { + * vid.loop(); + * vid.volume(0); + * } + * </code></div> + */ + p5.prototype.createVideo = function(src, callback) { + p5._validateParameters('createVideo', arguments); + return createMedia(this, 'video', src, callback); + }; + + /** AUDIO STUFF **/ + + /** + * Creates a hidden HTML5 <audio> element in the DOM for simple audio + * playback. Appends to the container node if one is specified, + * otherwise appends to body. The first parameter + * can be either a single string path to a audio file, or an array of string + * paths to different formats of the same audio. This is useful for ensuring + * that your audio can play across different browsers, as each supports + * different formats. See <a href='https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats'>this + * page for further information about supported formats</a>. + * + * @method createAudio + * @param {String|String[]} [src] path to an audio file, or array of paths + * for supporting different browsers + * @param {Function} [callback] callback function to be called upon + * 'canplaythrough' event fire, that is, when the + * browser can play the media, and estimates that + * enough data has been loaded to play the media + * up to its end without having to stop for + * further buffering of content + * @return {p5.MediaElement} pointer to audio <a href="#/p5.Element">p5.Element</a> + * @example + * <div><code> + * var ele; + * function setup() { + * ele = createAudio('assets/beat.mp3'); + * + * // here we set the element to autoplay + * // The element will play as soon + * // as it is able to do so. + * ele.autoplay(true); + * } + * </code></div> + */ + p5.prototype.createAudio = function(src, callback) { + p5._validateParameters('createAudio', arguments); + return createMedia(this, 'audio', src, callback); + }; + + /** CAMERA STUFF **/ + + /** + * @property {String} VIDEO + * @final + * @category Constants + */ + p5.prototype.VIDEO = 'video'; + /** + * @property {String} AUDIO + * @final + * @category Constants + */ + p5.prototype.AUDIO = 'audio'; + + // from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia + // Older browsers might not implement mediaDevices at all, so we set an empty object first + if (navigator.mediaDevices === undefined) { + navigator.mediaDevices = {}; + } + + // Some browsers partially implement mediaDevices. We can't just assign an object + // with getUserMedia as it would overwrite existing properties. + // Here, we will just add the getUserMedia property if it's missing. + if (navigator.mediaDevices.getUserMedia === undefined) { + navigator.mediaDevices.getUserMedia = function(constraints) { + // First get ahold of the legacy getUserMedia, if present + var getUserMedia = + navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + + // Some browsers just don't implement it - return a rejected promise with an error + // to keep a consistent interface + if (!getUserMedia) { + return Promise.reject( + new Error('getUserMedia is not implemented in this browser') + ); + } + + // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise + return new Promise(function(resolve, reject) { + getUserMedia.call(navigator, constraints, resolve, reject); + }); + }; + } + + /** + * <p>Creates a new HTML5 <video> element that contains the audio/video + * feed from a webcam. The element is separate from the canvas and is + * displayed by default. The element can be hidden using .<a href="#/p5.Element/hide">hide()</a>. The feed + * can be drawn onto the canvas using <a href="#/p5/image">image()</a>. The loadedmetadata property can + * be used to detect when the element has fully loaded (see second example).</p> + * <p>More specific properties of the feed can be passing in a Constraints object. + * See the + * <a href='http://w3c.github.io/mediacapture-main/getusermedia.html#media-track-constraints'> W3C + * spec</a> for possible properties. Note that not all of these are supported + * by all browsers.</p> + * <p>Security note: A new browser security specification requires that getUserMedia, + * which is behind <a href="#/p5/createCapture">createCapture()</a>, only works when you're running the code locally, + * or on HTTPS. Learn more <a href='http://stackoverflow.com/questions/34197653/getusermedia-in-chrome-47-without-using-https'>here</a> + * and <a href='https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia'>here</a>.</p> + * + * @method createCapture + * @param {String|Constant|Object} type type of capture, either VIDEO or + * AUDIO if none specified, default both, + * or a Constraints object + * @param {Function} [callback] function to be called once + * stream has loaded + * @return {p5.Element} capture video <a href="#/p5.Element">p5.Element</a> + * @example + * <div class='norender notest'><code> + * var capture; + * + * function setup() { + * createCanvas(480, 480); + * capture = createCapture(VIDEO); + * capture.hide(); + * } + * + * function draw() { + * image(capture, 0, 0, width, width * capture.height / capture.width); + * filter(INVERT); + * } + * </code></div> + * <div class='norender notest'><code> + * function setup() { + * createCanvas(480, 120); + * var constraints = { + * video: { + * mandatory: { + * minWidth: 1280, + * minHeight: 720 + * }, + * optional: [{ maxFrameRate: 10 }] + * }, + * audio: true + * }; + * createCapture(constraints, function(stream) { + * console.log(stream); + * }); + * } + * </code></div> + * <code><div class='norender notest'> + * var capture; + * + * function setup() { + * createCanvas(640, 480); + * capture = createCapture(VIDEO); + * } + * function draw() { + * background(0); + * if (capture.loadedmetadata) { + * var c = capture.get(0, 0, 100, 100); + * image(c, 0, 0); + * } + * } + * </code></div> + */ + p5.prototype.createCapture = function() { + p5._validateParameters('createCapture', arguments); + var useVideo = true; + var useAudio = true; + var constraints; + var cb; + for (var i = 0; i < arguments.length; i++) { + if (arguments[i] === p5.prototype.VIDEO) { + useAudio = false; + } else if (arguments[i] === p5.prototype.AUDIO) { + useVideo = false; + } else if (typeof arguments[i] === 'object') { + constraints = arguments[i]; + } else if (typeof arguments[i] === 'function') { + cb = arguments[i]; + } + } + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + var elt = document.createElement('video'); + // required to work in iOS 11 & up: + elt.setAttribute('playsinline', ''); + + if (!constraints) { + constraints = { video: useVideo, audio: useAudio }; + } + + navigator.mediaDevices.getUserMedia(constraints).then( + function(stream) { + try { + if ('srcObject' in elt) { + elt.srcObject = stream; + } else { + elt.src = window.URL.createObjectURL(stream); + } + } catch (err) { + elt.src = stream; + } + }, + function(e) { + console.log(e); + } + ); + } else { + throw 'getUserMedia not supported in this browser'; + } + var c = addElement(elt, this, true); + c.loadedmetadata = false; + // set width and height onload metadata + elt.addEventListener('loadedmetadata', function() { + elt.play(); + if (elt.width) { + c.width = elt.videoWidth = elt.width; + c.height = elt.videoHeight = elt.height; + } else { + c.width = c.elt.width = elt.videoWidth; + c.height = c.elt.height = elt.videoHeight; + } + c.loadedmetadata = true; + if (cb) { + cb(elt.srcObject); + } + }); + return c; + }; + + /** + * Creates element with given tag in the DOM with given content. + * Appends to the container node if one is specified, otherwise + * appends to body. + * + * @method createElement + * @param {String} tag tag for the new element + * @param {String} [content] html content to be inserted into the element + * @return {p5.Element} pointer to <a href="#/p5.Element">p5.Element</a> holding created node + * @example + * <div class='norender'><code> + * createElement('h2', 'im an h2 p5.element!'); + * </code></div> + */ + p5.prototype.createElement = function(tag, content) { + p5._validateParameters('createElement', arguments); + var elt = document.createElement(tag); + if (typeof content !== 'undefined') { + elt.innerHTML = content; + } + return addElement(elt, this); + }; + + // ============================================================================= + // p5.Element additions + // ============================================================================= + /** + * + * Adds specified class to the element. + * + * @for p5.Element + * @method addClass + * @param {String} class name of class to add + * @chainable + * @example + * <div class='norender'><code> + * var div = createDiv('div'); + * div.addClass('myClass'); + * </code></div> + */ + p5.Element.prototype.addClass = function(c) { + if (this.elt.className) { + if (!this.hasClass(c)) { + this.elt.className = this.elt.className + ' ' + c; + } + } else { + this.elt.className = c; + } + return this; + }; + + /** + * + * Removes specified class from the element. + * + * @method removeClass + * @param {String} class name of class to remove + * @chainable + * @example + * <div class='norender'><code> + * // In this example, a class is set when the div is created + * // and removed when mouse is pressed. This could link up + * // with a CSS style rule to toggle style properties. + * + * var div; + * + * function setup() { + * div = createDiv('div'); + * div.addClass('myClass'); + * } + * + * function mousePressed() { + * div.removeClass('myClass'); + * } + * </code></div> + */ + p5.Element.prototype.removeClass = function(c) { + // Note: Removing a class that does not exist does NOT throw an error in classList.remove method + this.elt.classList.remove(c); + return this; + }; + + /** + * + * Checks if specified class already set to element + * + * @method hasClass + * @returns {boolean} a boolean value if element has specified class + * @param c {String} class name of class to check + * @example + * <div class='norender'><code> + * var div; + * + * function setup() { + * div = createDiv('div'); + * div.addClass('show'); + * } + * + * function mousePressed() { + * if (div.hasClass('show')) { + * div.addClass('show'); + * } else { + * div.removeClass('show'); + * } + * } + * </code></div> + */ + p5.Element.prototype.hasClass = function(c) { + return this.elt.classList.contains(c); + }; + + /** + * + * Toggles element class + * + * @method toggleClass + * @param c {String} class name to toggle + * @chainable + * @example + * <div class='norender'><code> + * var div; + * + * function setup() { + * div = createDiv('div'); + * div.addClass('show'); + * } + * + * function mousePressed() { + * div.toggleClass('show'); + * } + * </code></div> + */ + p5.Element.prototype.toggleClass = function(c) { + // classList also has a toggle() method, but we cannot use that yet as support is unclear. + // See https://github.com/processing/p5.js/issues/3631 + // this.elt.classList.toggle(c); + if (this.elt.classList.contains(c)) { + this.elt.classList.remove(c); + } else { + this.elt.classList.add(c); + } + return this; + }; + + /** + * + * Attaches the element as a child to the parent specified. + * Accepts either a string ID, DOM node, or <a href="#/p5.Element">p5.Element</a>. + * If no argument is specified, an array of children DOM nodes is returned. + * + * @method child + * @returns {Node[]} an array of child nodes + * @example + * <div class='norender'><code> + * var div0 = createDiv('this is the parent'); + * var div1 = createDiv('this is the child'); + * div0.child(div1); // use p5.Element + * </code></div> + * <div class='norender'><code> + * var div0 = createDiv('this is the parent'); + * var div1 = createDiv('this is the child'); + * div1.id('apples'); + * div0.child('apples'); // use id + * </code></div> + * <div class='norender notest'><code> + * // this example assumes there is a div already on the page + * // with id "myChildDiv" + * var div0 = createDiv('this is the parent'); + * var elt = document.getElementById('myChildDiv'); + * div0.child(elt); // use element from page + * </code></div> + */ + /** + * @method child + * @param {String|p5.Element} [child] the ID, DOM node, or <a href="#/p5.Element">p5.Element</a> + * to add to the current element + * @chainable + */ + p5.Element.prototype.child = function(c) { + if (typeof c === 'undefined') { + return this.elt.childNodes; + } + if (typeof c === 'string') { + if (c[0] === '#') { + c = c.substring(1); + } + c = document.getElementById(c); + } else if (c instanceof p5.Element) { + c = c.elt; + } + this.elt.appendChild(c); + return this; + }; + + /** + * Centers a p5 Element either vertically, horizontally, + * or both, relative to its parent or according to + * the body if the Element has no parent. If no argument is passed + * the Element is aligned both vertically and horizontally. + * + * @method center + * @param {String} [align] passing 'vertical', 'horizontal' aligns element accordingly + * @chainable + * + * @example + * <div><code> + * function setup() { + * var div = createDiv('').size(10, 10); + * div.style('background-color', 'orange'); + * div.center(); + * } + * </code></div> + */ + p5.Element.prototype.center = function(align) { + var style = this.elt.style.display; + var hidden = this.elt.style.display === 'none'; + var parentHidden = this.parent().style.display === 'none'; + var pos = { x: this.elt.offsetLeft, y: this.elt.offsetTop }; + + if (hidden) this.show(); + + this.elt.style.display = 'block'; + this.position(0, 0); + + if (parentHidden) this.parent().style.display = 'block'; + + var wOffset = Math.abs(this.parent().offsetWidth - this.elt.offsetWidth); + var hOffset = Math.abs(this.parent().offsetHeight - this.elt.offsetHeight); + var y = pos.y; + var x = pos.x; + + if (align === 'both' || align === undefined) { + this.position(wOffset / 2, hOffset / 2); + } else if (align === 'horizontal') { + this.position(wOffset / 2, y); + } else if (align === 'vertical') { + this.position(x, hOffset / 2); + } + + this.style('display', style); + + if (hidden) this.hide(); + + if (parentHidden) this.parent().style.display = 'none'; + + return this; + }; + + /** + * + * If an argument is given, sets the inner HTML of the element, + * replacing any existing html. If true is included as a second + * argument, html is appended instead of replacing existing html. + * If no arguments are given, returns + * the inner HTML of the element. + * + * @for p5.Element + * @method html + * @returns {String} the inner HTML of the element + * @example + * <div class='norender'><code> + * var div = createDiv('').size(100, 100); + * div.html('hi'); + * </code></div> + * <div class='norender'><code> + * var div = createDiv('Hello ').size(100, 100); + * div.html('World', true); + * </code></div> + */ + /** + * @method html + * @param {String} [html] the HTML to be placed inside the element + * @param {boolean} [append] whether to append HTML to existing + * @chainable + */ + p5.Element.prototype.html = function() { + if (arguments.length === 0) { + return this.elt.innerHTML; + } else if (arguments[1]) { + this.elt.innerHTML += arguments[0]; + return this; + } else { + this.elt.innerHTML = arguments[0]; + return this; + } + }; + + /** + * + * Sets the position of the element relative to (0, 0) of the + * window. Essentially, sets position:absolute and left and top + * properties of style. If no arguments given returns the x and y position + * of the element in an object. + * + * @method position + * @returns {Object} the x and y position of the element in an object + * @example + * <div><code class='norender'> + * function setup() { + * var cnv = createCanvas(100, 100); + * // positions canvas 50px to the right and 100px + * // below upper left corner of the window + * cnv.position(50, 100); + * } + * </code></div> + */ + /** + * @method position + * @param {Number} [x] x-position relative to upper left of window + * @param {Number} [y] y-position relative to upper left of window + * @chainable + */ + p5.Element.prototype.position = function() { + if (arguments.length === 0) { + return { x: this.elt.offsetLeft, y: this.elt.offsetTop }; + } else { + this.elt.style.position = 'absolute'; + this.elt.style.left = arguments[0] + 'px'; + this.elt.style.top = arguments[1] + 'px'; + this.x = arguments[0]; + this.y = arguments[1]; + return this; + } + }; + + /* Helper method called by p5.Element.style() */ + p5.Element.prototype._translate = function() { + this.elt.style.position = 'absolute'; + // save out initial non-translate transform styling + var transform = ''; + if (this.elt.style.transform) { + transform = this.elt.style.transform.replace(/translate3d\(.*\)/g, ''); + transform = transform.replace(/translate[X-Z]?\(.*\)/g, ''); + } + if (arguments.length === 2) { + this.elt.style.transform = + 'translate(' + arguments[0] + 'px, ' + arguments[1] + 'px)'; + } else if (arguments.length > 2) { + this.elt.style.transform = + 'translate3d(' + + arguments[0] + + 'px,' + + arguments[1] + + 'px,' + + arguments[2] + + 'px)'; + if (arguments.length === 3) { + this.elt.parentElement.style.perspective = '1000px'; + } else { + this.elt.parentElement.style.perspective = arguments[3] + 'px'; + } + } + // add any extra transform styling back on end + this.elt.style.transform += transform; + return this; + }; + + /* Helper method called by p5.Element.style() */ + p5.Element.prototype._rotate = function() { + // save out initial non-rotate transform styling + var transform = ''; + if (this.elt.style.transform) { + transform = this.elt.style.transform.replace(/rotate3d\(.*\)/g, ''); + transform = transform.replace(/rotate[X-Z]?\(.*\)/g, ''); + } + + if (arguments.length === 1) { + this.elt.style.transform = 'rotate(' + arguments[0] + 'deg)'; + } else if (arguments.length === 2) { + this.elt.style.transform = + 'rotate(' + arguments[0] + 'deg, ' + arguments[1] + 'deg)'; + } else if (arguments.length === 3) { + this.elt.style.transform = 'rotateX(' + arguments[0] + 'deg)'; + this.elt.style.transform += 'rotateY(' + arguments[1] + 'deg)'; + this.elt.style.transform += 'rotateZ(' + arguments[2] + 'deg)'; + } + // add remaining transform back on + this.elt.style.transform += transform; + return this; + }; + + /** + * Sets the given style (css) property (1st arg) of the element with the + * given value (2nd arg). If a single argument is given, .style() + * returns the value of the given property; however, if the single argument + * is given in css syntax ('text-align:center'), .style() sets the css + * appropriately. + * + * @method style + * @param {String} property property to be set + * @returns {String} value of property + * @example + * <div><code class='norender'> + * var myDiv = createDiv('I like pandas.'); + * myDiv.style('font-size', '18px'); + * myDiv.style('color', '#ff0000'); + * </code></div> + * <div><code class='norender'> + * var col = color(25, 23, 200, 50); + * var button = createButton('button'); + * button.style('background-color', col); + * button.position(10, 10); + * </code></div> + * <div><code class='norender'> + * var myDiv; + * function setup() { + * background(200); + * myDiv = createDiv('I like gray.'); + * myDiv.position(20, 20); + * } + * + * function draw() { + * myDiv.style('font-size', mouseX + 'px'); + * } + * </code></div> + */ + /** + * @method style + * @param {String} property + * @param {String|Number|p5.Color} value value to assign to property + * @return {String} current value of property, if no value is given as second argument + * @chainable + */ + p5.Element.prototype.style = function(prop, val) { + var self = this; + + if (val instanceof p5.Color) { + val = + 'rgba(' + + val.levels[0] + + ',' + + val.levels[1] + + ',' + + val.levels[2] + + ',' + + val.levels[3] / 255 + + ')'; + } + + if (typeof val === 'undefined') { + // input provided as single line string + if (prop.indexOf(':') === -1) { + var styles = window.getComputedStyle(self.elt); + var style = styles.getPropertyValue(prop); + return style; + } else { + var attrs = prop.split(';'); + for (var i = 0; i < attrs.length; i++) { + var parts = attrs[i].split(':'); + if (parts[0] && parts[1]) { + this.elt.style[parts[0].trim()] = parts[1].trim(); + } + } + } + } else { + // input provided as key,val pair + this.elt.style[prop] = val; + if ( + prop === 'width' || + prop === 'height' || + prop === 'left' || + prop === 'top' + ) { + var numVal = val.replace(/\D+/g, ''); + this[prop] = parseInt(numVal, 10); + } + } + return this; + }; + + /** + * + * Adds a new attribute or changes the value of an existing attribute + * on the specified element. If no value is specified, returns the + * value of the given attribute, or null if attribute is not set. + * + * @method attribute + * @return {String} value of attribute + * + * @example + * <div class='norender'><code> + * var myDiv = createDiv('I like pandas.'); + * myDiv.attribute('align', 'center'); + * </code></div> + */ + /** + * @method attribute + * @param {String} attr attribute to set + * @param {String} value value to assign to attribute + * @chainable + */ + p5.Element.prototype.attribute = function(attr, value) { + //handling for checkboxes and radios to ensure options get + //attributes not divs + if ( + this.elt.firstChild != null && + (this.elt.firstChild.type === 'checkbox' || + this.elt.firstChild.type === 'radio') + ) { + if (typeof value === 'undefined') { + return this.elt.firstChild.getAttribute(attr); + } else { + for (var i = 0; i < this.elt.childNodes.length; i++) { + this.elt.childNodes[i].setAttribute(attr, value); + } + } + } else if (typeof value === 'undefined') { + return this.elt.getAttribute(attr); + } else { + this.elt.setAttribute(attr, value); + return this; + } + }; + + /** + * + * Removes an attribute on the specified element. + * + * @method removeAttribute + * @param {String} attr attribute to remove + * @chainable + * + * @example + * <div><code> + * var button; + * var checkbox; + * + * function setup() { + * checkbox = createCheckbox('enable', true); + * checkbox.changed(enableButton); + * button = createButton('button'); + * button.position(10, 10); + * } + * + * function enableButton() { + * if (this.checked()) { + * // Re-enable the button + * button.removeAttribute('disabled'); + * } else { + * // Disable the button + * button.attribute('disabled', ''); + * } + * } + * </code></div> + */ + p5.Element.prototype.removeAttribute = function(attr) { + if ( + this.elt.firstChild != null && + (this.elt.firstChild.type === 'checkbox' || + this.elt.firstChild.type === 'radio') + ) { + for (var i = 0; i < this.elt.childNodes.length; i++) { + this.elt.childNodes[i].removeAttribute(attr); + } + } + this.elt.removeAttribute(attr); + return this; + }; + + /** + * Either returns the value of the element if no arguments + * given, or sets the value of the element. + * + * @method value + * @return {String|Number} value of the element + * @example + * <div class='norender'><code> + * // gets the value + * var inp; + * function setup() { + * inp = createInput(''); + * } + * + * function mousePressed() { + * print(inp.value()); + * } + * </code></div> + * <div class='norender'><code> + * // sets the value + * var inp; + * function setup() { + * inp = createInput('myValue'); + * } + * + * function mousePressed() { + * inp.value('myValue'); + * } + * </code></div> + */ + /** + * @method value + * @param {String|Number} value + * @chainable + */ + p5.Element.prototype.value = function() { + if (arguments.length > 0) { + this.elt.value = arguments[0]; + return this; + } else { + if (this.elt.type === 'range') { + return parseFloat(this.elt.value); + } else return this.elt.value; + } + }; + + /** + * + * Shows the current element. Essentially, setting display:block for the style. + * + * @method show + * @chainable + * @example + * <div class='norender'><code> + * var div = createDiv('div'); + * div.style('display', 'none'); + * div.show(); // turns display to block + * </code></div> + */ + p5.Element.prototype.show = function() { + this.elt.style.display = 'block'; + return this; + }; + + /** + * Hides the current element. Essentially, setting display:none for the style. + * + * @method hide + * @chainable + * @example + * <div class='norender'><code> + * var div = createDiv('this is a div'); + * div.hide(); + * </code></div> + */ + p5.Element.prototype.hide = function() { + this.elt.style.display = 'none'; + return this; + }; + + /** + * + * Sets the width and height of the element. AUTO can be used to + * only adjust one dimension at a time. If no arguments are given, it + * returns the width and height of the element in an object. In case of + * elements which need to be loaded, such as images, it is recommended + * to call the function after the element has finished loading. + * + * @method size + * @return {Object} the width and height of the element in an object + * @example + * <div class='norender'><code> + * let div = createDiv('this is a div'); + * div.size(100, 100); + * let img = createImg('assets/laDefense.jpg', () => { + * img.size(10, AUTO); + * }); + * </code></div> + */ + /** + * @method size + * @param {Number|Constant} w width of the element, either AUTO, or a number + * @param {Number|Constant} [h] height of the element, either AUTO, or a number + * @chainable + */ + p5.Element.prototype.size = function(w, h) { + if (arguments.length === 0) { + return { width: this.elt.offsetWidth, height: this.elt.offsetHeight }; + } else { + var aW = w; + var aH = h; + var AUTO = p5.prototype.AUTO; + if (aW !== AUTO || aH !== AUTO) { + if (aW === AUTO) { + aW = h * this.width / this.height; + } else if (aH === AUTO) { + aH = w * this.height / this.width; + } + // set diff for cnv vs normal div + if (this.elt instanceof HTMLCanvasElement) { + var j = {}; + var k = this.elt.getContext('2d'); + var prop; + for (prop in k) { + j[prop] = k[prop]; + } + this.elt.setAttribute('width', aW * this._pInst._pixelDensity); + this.elt.setAttribute('height', aH * this._pInst._pixelDensity); + this.elt.style.width = aW + 'px'; + this.elt.style.height = aH + 'px'; + this._pInst.scale( + this._pInst._pixelDensity, + this._pInst._pixelDensity + ); + for (prop in j) { + this.elt.getContext('2d')[prop] = j[prop]; + } + } else { + this.elt.style.width = aW + 'px'; + this.elt.style.height = aH + 'px'; + this.elt.width = aW; + this.elt.height = aH; + } + + this.width = this.elt.offsetWidth; + this.height = this.elt.offsetHeight; + + if (this._pInst && this._pInst._curElement) { + // main canvas associated with p5 instance + if (this._pInst._curElement.elt === this.elt) { + this._pInst._setProperty('width', this.elt.offsetWidth); + this._pInst._setProperty('height', this.elt.offsetHeight); + } + } + } + return this; + } + }; + + /** + * Removes the element and deregisters all listeners. + * @method remove + * @example + * <div class='norender'><code> + * var myDiv = createDiv('this is some text'); + * myDiv.remove(); + * </code></div> + */ + p5.Element.prototype.remove = function() { + // deregister events + for (var ev in this._events) { + this.elt.removeEventListener(ev, this._events[ev]); + } + if (this.elt.parentNode) { + this.elt.parentNode.removeChild(this.elt); + } + delete this; + }; + + /** + * Registers a callback that gets called every time a file that is + * dropped on the element has been loaded. + * p5 will load every dropped file into memory and pass it as a p5.File object to the callback. + * Multiple files dropped at the same time will result in multiple calls to the callback. + * + * You can optionally pass a second callback which will be registered to the raw + * <a href="https://developer.mozilla.org/en-US/docs/Web/Events/drop">drop</a> event. + * The callback will thus be provided the original + * <a href="https://developer.mozilla.org/en-US/docs/Web/API/DragEvent">DragEvent</a>. + * Dropping multiple files at the same time will trigger the second callback once per drop, + * whereas the first callback will trigger for each loaded file. + * + * @method drop + * @param {Function} callback callback to receive loaded file, called for each file dropped. + * @param {Function} [fxn] callback triggered once when files are dropped with the drop event. + * @chainable + * @example + * <div><code> + * function setup() { + * var c = createCanvas(100, 100); + * background(200); + * textAlign(CENTER); + * text('drop file', width / 2, height / 2); + * c.drop(gotFile); + * } + * + * function gotFile(file) { + * background(200); + * text('received file:', width / 2, height / 2); + * text(file.name, width / 2, height / 2 + 50); + * } + * </code></div> + * + * <div><code> + * var img; + * + * function setup() { + * var c = createCanvas(100, 100); + * background(200); + * textAlign(CENTER); + * text('drop image', width / 2, height / 2); + * c.drop(gotFile); + * } + * + * function draw() { + * if (img) { + * image(img, 0, 0, width, height); + * } + * } + * + * function gotFile(file) { + * img = createImg(file.data).hide(); + * } + * </code></div> + * + * @alt + * Canvas turns into whatever image is dragged/dropped onto it. + */ + p5.Element.prototype.drop = function(callback, fxn) { + // Is the file stuff supported? + if (window.File && window.FileReader && window.FileList && window.Blob) { + if (!this._dragDisabled) { + this._dragDisabled = true; + + var preventDefault = function(evt) { + evt.preventDefault(); + }; + + // If you want to be able to drop you've got to turn off + // a lot of default behavior. + // avoid `attachListener` here, since it overrides other handlers. + this.elt.addEventListener('dragover', preventDefault); + + // If this is a drag area we need to turn off the default behavior + this.elt.addEventListener('dragleave', preventDefault); + } + + // Deal with the files + p5.Element._attachListener( + 'drop', + function(evt) { + evt.preventDefault(); + // Call the second argument as a callback that receives the raw drop event + if (typeof fxn === 'function') { + fxn.call(this, evt); + } + // A FileList + var files = evt.dataTransfer.files; + + // Load each one and trigger the callback + for (var i = 0; i < files.length; i++) { + var f = files[i]; + p5.File._load(f, callback); + } + }, + this + ); + } else { + console.log('The File APIs are not fully supported in this browser.'); + } + + return this; + }; + + // ============================================================================= + // p5.MediaElement additions + // ============================================================================= + + /** + * Extends <a href="#/p5.Element">p5.Element</a> to handle audio and video. In addition to the methods + * of <a href="#/p5.Element">p5.Element</a>, it also contains methods for controlling media. It is not + * called directly, but <a href="#/p5.MediaElement">p5.MediaElement</a>s are created by calling <a href="#/p5/createVideo">createVideo</a>, + * <a href="#/p5/createAudio">createAudio</a>, and <a href="#/p5/createCapture">createCapture</a>. + * + * @class p5.MediaElement + * @constructor + * @param {String} elt DOM node that is wrapped + */ + p5.MediaElement = function(elt, pInst) { + p5.Element.call(this, elt, pInst); + + var self = this; + this.elt.crossOrigin = 'anonymous'; + + this._prevTime = 0; + this._cueIDCounter = 0; + this._cues = []; + this._pixelsState = this; + this._pixelDensity = 1; + this._modified = false; + this._pixelsDirty = true; + this._pixelsTime = -1; // the time at which we last updated 'pixels' + + /** + * Path to the media element source. + * + * @property src + * @return {String} src + * @example + * <div><code> + * var ele; + * + * function setup() { + * background(250); + * + * //p5.MediaElement objects are usually created + * //by calling the createAudio(), createVideo(), + * //and createCapture() functions. + * + * //In this example we create + * //a new p5.MediaElement via createAudio(). + * ele = createAudio('assets/beat.mp3'); + * + * //We'll set up our example so that + * //when you click on the text, + * //an alert box displays the MediaElement's + * //src field. + * textAlign(CENTER); + * text('Click Me!', width / 2, height / 2); + * } + * + * function mouseClicked() { + * //here we test if the mouse is over the + * //canvas element when it's clicked + * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { + * //Show our p5.MediaElement's src field + * alert(ele.src); + * } + * } + * </code></div> + */ + Object.defineProperty(self, 'src', { + get: function() { + var firstChildSrc = self.elt.children[0].src; + var srcVal = self.elt.src === window.location.href ? '' : self.elt.src; + var ret = + firstChildSrc === window.location.href ? srcVal : firstChildSrc; + return ret; + }, + set: function(newValue) { + for (var i = 0; i < self.elt.children.length; i++) { + self.elt.removeChild(self.elt.children[i]); + } + var source = document.createElement('source'); + source.src = newValue; + elt.appendChild(source); + self.elt.src = newValue; + self.modified = true; + } + }); + + // private _onended callback, set by the method: onended(callback) + self._onended = function() {}; + self.elt.onended = function() { + self._onended(self); + }; + }; + p5.MediaElement.prototype = Object.create(p5.Element.prototype); + + /** + * Play an HTML5 media element. + * + * @method play + * @chainable + * @example + * <div><code> + * var ele; + * + * function setup() { + * //p5.MediaElement objects are usually created + * //by calling the createAudio(), createVideo(), + * //and createCapture() functions. + * + * //In this example we create + * //a new p5.MediaElement via createAudio(). + * ele = createAudio('assets/beat.mp3'); + * + * background(250); + * textAlign(CENTER); + * text('Click to Play!', width / 2, height / 2); + * } + * + * function mouseClicked() { + * //here we test if the mouse is over the + * //canvas element when it's clicked + * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { + * //Here we call the play() function on + * //the p5.MediaElement we created above. + * //This will start the audio sample. + * ele.play(); + * + * background(200); + * text('You clicked Play!', width / 2, height / 2); + * } + * } + * </code></div> + */ + p5.MediaElement.prototype.play = function() { + if (this.elt.currentTime === this.elt.duration) { + this.elt.currentTime = 0; + } + var promise; + if (this.elt.readyState > 1) { + promise = this.elt.play(); + } else { + // in Chrome, playback cannot resume after being stopped and must reload + this.elt.load(); + promise = this.elt.play(); + } + if (promise && promise.catch) { + promise.catch(function(e) { + console.log( + 'WARN: Element play method raised an error asynchronously', + e + ); + }); + } + return this; + }; + + /** + * Stops an HTML5 media element (sets current time to zero). + * + * @method stop + * @chainable + * @example + * <div><code> + * //This example both starts + * //and stops a sound sample + * //when the user clicks the canvas + * + * //We will store the p5.MediaElement + * //object in here + * var ele; + * + * //while our audio is playing, + * //this will be set to true + * var sampleIsPlaying = false; + * + * function setup() { + * //Here we create a p5.MediaElement object + * //using the createAudio() function. + * ele = createAudio('assets/beat.mp3'); + * background(200); + * textAlign(CENTER); + * text('Click to play!', width / 2, height / 2); + * } + * + * function mouseClicked() { + * //here we test if the mouse is over the + * //canvas element when it's clicked + * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { + * background(200); + * + * if (sampleIsPlaying) { + * //if the sample is currently playing + * //calling the stop() function on + * //our p5.MediaElement will stop + * //it and reset its current + * //time to 0 (i.e. it will start + * //at the beginning the next time + * //you play it) + * ele.stop(); + * + * sampleIsPlaying = false; + * text('Click to play!', width / 2, height / 2); + * } else { + * //loop our sound element until we + * //call ele.stop() on it. + * ele.loop(); + * + * sampleIsPlaying = true; + * text('Click to stop!', width / 2, height / 2); + * } + * } + * } + * </code></div> + */ + p5.MediaElement.prototype.stop = function() { + this.elt.pause(); + this.elt.currentTime = 0; + return this; + }; + + /** + * Pauses an HTML5 media element. + * + * @method pause + * @chainable + * @example + * <div><code> + * //This example both starts + * //and pauses a sound sample + * //when the user clicks the canvas + * + * //We will store the p5.MediaElement + * //object in here + * var ele; + * + * //while our audio is playing, + * //this will be set to true + * var sampleIsPlaying = false; + * + * function setup() { + * //Here we create a p5.MediaElement object + * //using the createAudio() function. + * ele = createAudio('assets/lucky_dragons.mp3'); + * background(200); + * textAlign(CENTER); + * text('Click to play!', width / 2, height / 2); + * } + * + * function mouseClicked() { + * //here we test if the mouse is over the + * //canvas element when it's clicked + * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { + * background(200); + * + * if (sampleIsPlaying) { + * //Calling pause() on our + * //p5.MediaElement will stop it + * //playing, but when we call the + * //loop() or play() functions + * //the sample will start from + * //where we paused it. + * ele.pause(); + * + * sampleIsPlaying = false; + * text('Click to resume!', width / 2, height / 2); + * } else { + * //loop our sound element until we + * //call ele.pause() on it. + * ele.loop(); + * + * sampleIsPlaying = true; + * text('Click to pause!', width / 2, height / 2); + * } + * } + * } + * </code></div> + */ + p5.MediaElement.prototype.pause = function() { + this.elt.pause(); + return this; + }; + + /** + * Set 'loop' to true for an HTML5 media element, and starts playing. + * + * @method loop + * @chainable + * @example + * <div><code> + * //Clicking the canvas will loop + * //the audio sample until the user + * //clicks again to stop it + * + * //We will store the p5.MediaElement + * //object in here + * var ele; + * + * //while our audio is playing, + * //this will be set to true + * var sampleIsLooping = false; + * + * function setup() { + * //Here we create a p5.MediaElement object + * //using the createAudio() function. + * ele = createAudio('assets/lucky_dragons.mp3'); + * background(200); + * textAlign(CENTER); + * text('Click to loop!', width / 2, height / 2); + * } + * + * function mouseClicked() { + * //here we test if the mouse is over the + * //canvas element when it's clicked + * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { + * background(200); + * + * if (!sampleIsLooping) { + * //loop our sound element until we + * //call ele.stop() on it. + * ele.loop(); + * + * sampleIsLooping = true; + * text('Click to stop!', width / 2, height / 2); + * } else { + * ele.stop(); + * + * sampleIsLooping = false; + * text('Click to loop!', width / 2, height / 2); + * } + * } + * } + * </code></div> + */ + p5.MediaElement.prototype.loop = function() { + this.elt.setAttribute('loop', true); + this.play(); + return this; + }; + /** + * Set 'loop' to false for an HTML5 media element. Element will stop + * when it reaches the end. + * + * @method noLoop + * @chainable + * @example + * <div><code> + * //This example both starts + * //and stops loop of sound sample + * //when the user clicks the canvas + * + * //We will store the p5.MediaElement + * //object in here + * var ele; + * //while our audio is playing, + * //this will be set to true + * var sampleIsPlaying = false; + * + * function setup() { + * //Here we create a p5.MediaElement object + * //using the createAudio() function. + * ele = createAudio('assets/beat.mp3'); + * background(200); + * textAlign(CENTER); + * text('Click to play!', width / 2, height / 2); + * } + * + * function mouseClicked() { + * //here we test if the mouse is over the + * //canvas element when it's clicked + * if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) { + * background(200); + * + * if (sampleIsPlaying) { + * ele.noLoop(); + * text('No more Loops!', width / 2, height / 2); + * } else { + * ele.loop(); + * sampleIsPlaying = true; + * text('Click to stop looping!', width / 2, height / 2); + * } + * } + * } + * </code></div> + * + */ + p5.MediaElement.prototype.noLoop = function() { + this.elt.setAttribute('loop', false); + return this; + }; + + /** + * Set HTML5 media element to autoplay or not. + * + * @method autoplay + * @param {Boolean} autoplay whether the element should autoplay + * @chainable + */ + p5.MediaElement.prototype.autoplay = function(val) { + this.elt.setAttribute('autoplay', val); + return this; + }; + + /** + * Sets volume for this HTML5 media element. If no argument is given, + * returns the current volume. + * + * @method volume + * @return {Number} current volume + * + * @example + * <div><code> + * var ele; + * function setup() { + * // p5.MediaElement objects are usually created + * // by calling the createAudio(), createVideo(), + * // and createCapture() functions. + * // In this example we create + * // a new p5.MediaElement via createAudio(). + * ele = createAudio('assets/lucky_dragons.mp3'); + * background(250); + * textAlign(CENTER); + * text('Click to Play!', width / 2, height / 2); + * } + * function mouseClicked() { + * // Here we call the volume() function + * // on the sound element to set its volume + * // Volume must be between 0.0 and 1.0 + * ele.volume(0.2); + * ele.play(); + * background(200); + * text('You clicked Play!', width / 2, height / 2); + * } + * </code></div> + * <div><code> + * var audio; + * var counter = 0; + * + * function loaded() { + * audio.play(); + * } + * + * function setup() { + * audio = createAudio('assets/lucky_dragons.mp3', loaded); + * textAlign(CENTER); + * } + * + * function draw() { + * if (counter === 0) { + * background(0, 255, 0); + * text('volume(0.9)', width / 2, height / 2); + * } else if (counter === 1) { + * background(255, 255, 0); + * text('volume(0.5)', width / 2, height / 2); + * } else if (counter === 2) { + * background(255, 0, 0); + * text('volume(0.1)', width / 2, height / 2); + * } + * } + * + * function mousePressed() { + * counter++; + * if (counter === 0) { + * audio.volume(0.9); + * } else if (counter === 1) { + * audio.volume(0.5); + * } else if (counter === 2) { + * audio.volume(0.1); + * } else { + * counter = 0; + * audio.volume(0.9); + * } + * } + * </code> + * </div> + */ + /** + * @method volume + * @param {Number} val volume between 0.0 and 1.0 + * @chainable + */ + p5.MediaElement.prototype.volume = function(val) { + if (typeof val === 'undefined') { + return this.elt.volume; + } else { + this.elt.volume = val; + } + }; + + /** + * If no arguments are given, returns the current playback speed of the + * element. The speed parameter sets the speed where 2.0 will play the + * element twice as fast, 0.5 will play at half the speed, and -1 will play + * the element in normal speed in reverse.(Note that not all browsers support + * backward playback and even if they do, playback might not be smooth.) + * + * @method speed + * @return {Number} current playback speed of the element + * + * @example + * <div class='norender notest'><code> + * //Clicking the canvas will loop + * //the audio sample until the user + * //clicks again to stop it + * + * //We will store the p5.MediaElement + * //object in here + * var ele; + * var button; + * + * function setup() { + * createCanvas(710, 400); + * //Here we create a p5.MediaElement object + * //using the createAudio() function. + * ele = createAudio('assets/beat.mp3'); + * ele.loop(); + * background(200); + * + * button = createButton('2x speed'); + * button.position(100, 68); + * button.mousePressed(twice_speed); + * + * button = createButton('half speed'); + * button.position(200, 68); + * button.mousePressed(half_speed); + * + * button = createButton('reverse play'); + * button.position(300, 68); + * button.mousePressed(reverse_speed); + * + * button = createButton('STOP'); + * button.position(400, 68); + * button.mousePressed(stop_song); + * + * button = createButton('PLAY!'); + * button.position(500, 68); + * button.mousePressed(play_speed); + * } + * + * function twice_speed() { + * ele.speed(2); + * } + * + * function half_speed() { + * ele.speed(0.5); + * } + * + * function reverse_speed() { + * ele.speed(-1); + * } + * + * function stop_song() { + * ele.stop(); + * } + * + * function play_speed() { + * ele.play(); + * } + * </code></div> + */ + /** + * @method speed + * @param {Number} speed speed multiplier for element playback + * @chainable + */ + p5.MediaElement.prototype.speed = function(val) { + if (typeof val === 'undefined') { + return this.presetPlaybackRate || this.elt.playbackRate; + } else { + if (this.loadedmetadata) { + this.elt.playbackRate = val; + } else { + this.presetPlaybackRate = val; + } + } + }; + + /** + * If no arguments are given, returns the current time of the element. + * If an argument is given the current time of the element is set to it. + * + * @method time + * @return {Number} current time (in seconds) + * + * @example + * <div><code> + * var ele; + * var beginning = true; + * function setup() { + * //p5.MediaElement objects are usually created + * //by calling the createAudio(), createVideo(), + * //and createCapture() functions. + * + * //In this example we create + * //a new p5.MediaElement via createAudio(). + * ele = createAudio('assets/lucky_dragons.mp3'); + * background(250); + * textAlign(CENTER); + * text('start at beginning', width / 2, height / 2); + * } + * + * // this function fires with click anywhere + * function mousePressed() { + * if (beginning === true) { + * // here we start the sound at the beginning + * // time(0) is not necessary here + * // as this produces the same result as + * // play() + * ele.play().time(0); + * background(200); + * text('jump 2 sec in', width / 2, height / 2); + * beginning = false; + * } else { + * // here we jump 2 seconds into the sound + * ele.play().time(2); + * background(250); + * text('start at beginning', width / 2, height / 2); + * beginning = true; + * } + * } + * </code></div> + */ + /** + * @method time + * @param {Number} time time to jump to (in seconds) + * @chainable + */ + p5.MediaElement.prototype.time = function(val) { + if (typeof val === 'undefined') { + return this.elt.currentTime; + } else { + this.elt.currentTime = val; + return this; + } + }; + + /** + * Returns the duration of the HTML5 media element. + * + * @method duration + * @return {Number} duration + * + * @example + * <div><code> + * var ele; + * function setup() { + * //p5.MediaElement objects are usually created + * //by calling the createAudio(), createVideo(), + * //and createCapture() functions. + * //In this example we create + * //a new p5.MediaElement via createAudio(). + * ele = createAudio('assets/doorbell.mp3'); + * background(250); + * textAlign(CENTER); + * text('Click to know the duration!', 10, 25, 70, 80); + * } + * function mouseClicked() { + * ele.play(); + * background(200); + * //ele.duration dislpays the duration + * text(ele.duration() + ' seconds', width / 2, height / 2); + * } + * </code></div> + */ + p5.MediaElement.prototype.duration = function() { + return this.elt.duration; + }; + p5.MediaElement.prototype.pixels = []; + p5.MediaElement.prototype._ensureCanvas = function() { + if (!this.canvas) { + this.canvas = document.createElement('canvas'); + this.drawingContext = this.canvas.getContext('2d'); + this.setModified(true); + } + if (this.loadedmetadata) { + // wait for metadata for w/h + if (this.canvas.width !== this.elt.width) { + this.canvas.width = this.elt.width; + this.canvas.height = this.elt.height; + this.width = this.canvas.width; + this.height = this.canvas.height; + this._pixelsDirty = true; + } + + var currentTime = this.elt.currentTime; + if (this._pixelsDirty || this._pixelsTime !== currentTime) { + // only update the pixels array if it's dirty, or + // if the video time has changed. + this._pixelsTime = currentTime; + this._pixelsDirty = true; + + this.drawingContext.drawImage( + this.elt, + 0, + 0, + this.canvas.width, + this.canvas.height + ); + this.setModified(true); + } + } + }; + p5.MediaElement.prototype.loadPixels = function() { + this._ensureCanvas(); + return p5.Renderer2D.prototype.loadPixels.apply(this, arguments); + }; + p5.MediaElement.prototype.updatePixels = function(x, y, w, h) { + if (this.loadedmetadata) { + // wait for metadata + this._ensureCanvas(); + p5.Renderer2D.prototype.updatePixels.call(this, x, y, w, h); + } + this.setModified(true); + return this; + }; + p5.MediaElement.prototype.get = function() { + this._ensureCanvas(); + return p5.Renderer2D.prototype.get.apply(this, arguments); + }; + p5.MediaElement.prototype._getPixel = function() { + this.loadPixels(); + return p5.Renderer2D.prototype._getPixel.apply(this, arguments); + }; + + p5.MediaElement.prototype.set = function(x, y, imgOrCol) { + if (this.loadedmetadata) { + // wait for metadata + this._ensureCanvas(); + p5.Renderer2D.prototype.set.call(this, x, y, imgOrCol); + this.setModified(true); + } + }; + p5.MediaElement.prototype.copy = function() { + this._ensureCanvas(); + p5.Renderer2D.prototype.copy.apply(this, arguments); + }; + p5.MediaElement.prototype.mask = function() { + this.loadPixels(); + this.setModified(true); + p5.Image.prototype.mask.apply(this, arguments); + }; + /** + * helper method for web GL mode to figure out if the element + * has been modified and might need to be re-uploaded to texture + * memory between frames. + * @method isModified + * @private + * @return {boolean} a boolean indicating whether or not the + * image has been updated or modified since last texture upload. + */ + p5.MediaElement.prototype.isModified = function() { + return this._modified; + }; + /** + * helper method for web GL mode to indicate that an element has been + * changed or unchanged since last upload. gl texture upload will + * set this value to false after uploading the texture; or might set + * it to true if metadata has become available but there is no actual + * texture data available yet.. + * @method setModified + * @param {boolean} val sets whether or not the element has been + * modified. + * @private + */ + p5.MediaElement.prototype.setModified = function(value) { + this._modified = value; + }; + /** + * Schedule an event to be called when the audio or video + * element reaches the end. If the element is looping, + * this will not be called. The element is passed in + * as the argument to the onended callback. + * + * @method onended + * @param {Function} callback function to call when the + * soundfile has ended. The + * media element will be passed + * in as the argument to the + * callback. + * @chainable + * @example + * <div><code> + * function setup() { + * var audioEl = createAudio('assets/beat.mp3'); + * audioEl.showControls(); + * audioEl.onended(sayDone); + * } + * + * function sayDone(elt) { + * alert('done playing ' + elt.src); + * } + * </code></div> + */ + p5.MediaElement.prototype.onended = function(callback) { + this._onended = callback; + return this; + }; + + /*** CONNECT TO WEB AUDIO API / p5.sound.js ***/ + + /** + * Send the audio output of this element to a specified audioNode or + * p5.sound object. If no element is provided, connects to p5's master + * output. That connection is established when this method is first called. + * All connections are removed by the .disconnect() method. + * + * This method is meant to be used with the p5.sound.js addon library. + * + * @method connect + * @param {AudioNode|Object} audioNode AudioNode from the Web Audio API, + * or an object from the p5.sound library + */ + p5.MediaElement.prototype.connect = function(obj) { + var audioContext, masterOutput; + + // if p5.sound exists, same audio context + if (typeof p5.prototype.getAudioContext === 'function') { + audioContext = p5.prototype.getAudioContext(); + masterOutput = p5.soundOut.input; + } else { + try { + audioContext = obj.context; + masterOutput = audioContext.destination; + } catch (e) { + throw 'connect() is meant to be used with Web Audio API or p5.sound.js'; + } + } + + // create a Web Audio MediaElementAudioSourceNode if none already exists + if (!this.audioSourceNode) { + this.audioSourceNode = audioContext.createMediaElementSource(this.elt); + + // connect to master output when this method is first called + this.audioSourceNode.connect(masterOutput); + } + + // connect to object if provided + if (obj) { + if (obj.input) { + this.audioSourceNode.connect(obj.input); + } else { + this.audioSourceNode.connect(obj); + } + } else { + // otherwise connect to master output of p5.sound / AudioContext + this.audioSourceNode.connect(masterOutput); + } + }; + + /** + * Disconnect all Web Audio routing, including to master output. + * This is useful if you want to re-route the output through + * audio effects, for example. + * + * @method disconnect + */ + p5.MediaElement.prototype.disconnect = function() { + if (this.audioSourceNode) { + this.audioSourceNode.disconnect(); + } else { + throw 'nothing to disconnect'; + } + }; + + /*** SHOW / HIDE CONTROLS ***/ + + /** + * Show the default MediaElement controls, as determined by the web browser. + * + * @method showControls + * @example + * <div><code> + * var ele; + * function setup() { + * //p5.MediaElement objects are usually created + * //by calling the createAudio(), createVideo(), + * //and createCapture() functions. + * //In this example we create + * //a new p5.MediaElement via createAudio() + * ele = createAudio('assets/lucky_dragons.mp3'); + * background(200); + * textAlign(CENTER); + * text('Click to Show Controls!', 10, 25, 70, 80); + * } + * function mousePressed() { + * ele.showControls(); + * background(200); + * text('Controls Shown', width / 2, height / 2); + * } + * </code></div> + */ + p5.MediaElement.prototype.showControls = function() { + // must set style for the element to show on the page + this.elt.style['text-align'] = 'inherit'; + this.elt.controls = true; + }; + + /** + * Hide the default mediaElement controls. + * @method hideControls + * @example + * <div><code> + * var ele; + * function setup() { + * //p5.MediaElement objects are usually created + * //by calling the createAudio(), createVideo(), + * //and createCapture() functions. + * //In this example we create + * //a new p5.MediaElement via createAudio() + * ele = createAudio('assets/lucky_dragons.mp3'); + * ele.showControls(); + * background(200); + * textAlign(CENTER); + * text('Click to hide Controls!', 10, 25, 70, 80); + * } + * function mousePressed() { + * ele.hideControls(); + * background(200); + * text('Controls hidden', width / 2, height / 2); + * } + * </code></div> + */ + p5.MediaElement.prototype.hideControls = function() { + this.elt.controls = false; + }; + + /*** SCHEDULE EVENTS ***/ + + // Cue inspired by JavaScript setTimeout, and the + // Tone.js Transport Timeline Event, MIT License Yotam Mann 2015 tonejs.org + var Cue = function(callback, time, id, val) { + this.callback = callback; + this.time = time; + this.id = id; + this.val = val; + }; + + /** + * Schedule events to trigger every time a MediaElement + * (audio/video) reaches a playback cue point. + * + * Accepts a callback function, a time (in seconds) at which to trigger + * the callback, and an optional parameter for the callback. + * + * Time will be passed as the first parameter to the callback function, + * and param will be the second parameter. + * + * + * @method addCue + * @param {Number} time Time in seconds, relative to this media + * element's playback. For example, to trigger + * an event every time playback reaches two + * seconds, pass in the number 2. This will be + * passed as the first parameter to + * the callback function. + * @param {Function} callback Name of a function that will be + * called at the given time. The callback will + * receive time and (optionally) param as its + * two parameters. + * @param {Object} [value] An object to be passed as the + * second parameter to the + * callback function. + * @return {Number} id ID of this cue, + * useful for removeCue(id) + * @example + * <div><code> + * // + * // + * function setup() { + * noCanvas(); + * + * var audioEl = createAudio('assets/beat.mp3'); + * audioEl.showControls(); + * + * // schedule three calls to changeBackground + * audioEl.addCue(0.5, changeBackground, color(255, 0, 0)); + * audioEl.addCue(1.0, changeBackground, color(0, 255, 0)); + * audioEl.addCue(2.5, changeBackground, color(0, 0, 255)); + * audioEl.addCue(3.0, changeBackground, color(0, 255, 255)); + * audioEl.addCue(4.2, changeBackground, color(255, 255, 0)); + * audioEl.addCue(5.0, changeBackground, color(255, 255, 0)); + * } + * + * function changeBackground(val) { + * background(val); + * } + * </code></div> + */ + p5.MediaElement.prototype.addCue = function(time, callback, val) { + var id = this._cueIDCounter++; + + var cue = new Cue(callback, time, id, val); + this._cues.push(cue); + + if (!this.elt.ontimeupdate) { + this.elt.ontimeupdate = this._onTimeUpdate.bind(this); + } + + return id; + }; + + /** + * Remove a callback based on its ID. The ID is returned by the + * addCue method. + * @method removeCue + * @param {Number} id ID of the cue, as returned by addCue + * @example + * <div><code> + * var audioEl, id1, id2; + * function setup() { + * background(255, 255, 255); + * audioEl = createAudio('assets/beat.mp3'); + * audioEl.showControls(); + * // schedule five calls to changeBackground + * id1 = audioEl.addCue(0.5, changeBackground, color(255, 0, 0)); + * audioEl.addCue(1.0, changeBackground, color(0, 255, 0)); + * audioEl.addCue(2.5, changeBackground, color(0, 0, 255)); + * audioEl.addCue(3.0, changeBackground, color(0, 255, 255)); + * id2 = audioEl.addCue(4.2, changeBackground, color(255, 255, 0)); + * text('Click to remove first and last Cue!', 10, 25, 70, 80); + * } + * function mousePressed() { + * audioEl.removeCue(id1); + * audioEl.removeCue(id2); + * } + * function changeBackground(val) { + * background(val); + * } + * </code></div> + */ + p5.MediaElement.prototype.removeCue = function(id) { + for (var i = 0; i < this._cues.length; i++) { + if (this._cues[i].id === id) { + console.log(id); + this._cues.splice(i, 1); + } + } + + if (this._cues.length === 0) { + this.elt.ontimeupdate = null; + } + }; + + /** + * Remove all of the callbacks that had originally been scheduled + * via the addCue method. + * @method clearCues + * @param {Number} id ID of the cue, as returned by addCue + * @example + * <div><code> + * var audioEl; + * function setup() { + * background(255, 255, 255); + * audioEl = createAudio('assets/beat.mp3'); + * //Show the default MediaElement controls, as determined by the web browser + * audioEl.showControls(); + * // schedule calls to changeBackground + * background(200); + * text('Click to change Cue!', 10, 25, 70, 80); + * audioEl.addCue(0.5, changeBackground, color(255, 0, 0)); + * audioEl.addCue(1.0, changeBackground, color(0, 255, 0)); + * audioEl.addCue(2.5, changeBackground, color(0, 0, 255)); + * audioEl.addCue(3.0, changeBackground, color(0, 255, 255)); + * audioEl.addCue(4.2, changeBackground, color(255, 255, 0)); + * } + * function mousePressed() { + * // here we clear the scheduled callbacks + * audioEl.clearCues(); + * // then we add some more callbacks + * audioEl.addCue(1, changeBackground, color(2, 2, 2)); + * audioEl.addCue(3, changeBackground, color(255, 255, 0)); + * } + * function changeBackground(val) { + * background(val); + * } + * </code></div> + */ + p5.MediaElement.prototype.clearCues = function() { + this._cues = []; + this.elt.ontimeupdate = null; + }; + + // private method that checks for cues to be fired if events + // have been scheduled using addCue(callback, time). + p5.MediaElement.prototype._onTimeUpdate = function() { + var playbackTime = this.time(); + + for (var i = 0; i < this._cues.length; i++) { + var callbackTime = this._cues[i].time; + var val = this._cues[i].val; + + if (this._prevTime < callbackTime && callbackTime <= playbackTime) { + // pass the scheduled callbackTime as parameter to the callback + this._cues[i].callback(val); + } + } + + this._prevTime = playbackTime; + }; + + /** + * Base class for a file. + * Used for Element.drop and createFileInput. + * + * @class p5.File + * @constructor + * @param {File} file File that is wrapped + */ + p5.File = function(file, pInst) { + /** + * Underlying File object. All normal File methods can be called on this. + * + * @property file + */ + this.file = file; + + this._pInst = pInst; + + // Splitting out the file type into two components + // This makes determining if image or text etc simpler + var typeList = file.type.split('/'); + /** + * File type (image, text, etc.) + * + * @property type + */ + this.type = typeList[0]; + /** + * File subtype (usually the file extension jpg, png, xml, etc.) + * + * @property subtype + */ + this.subtype = typeList[1]; + /** + * File name + * + * @property name + */ + this.name = file.name; + /** + * File size + * + * @property size + */ + this.size = file.size; + + /** + * URL string containing image data. + * + * @property data + */ + this.data = undefined; + }; + + p5.File._createLoader = function(theFile, callback) { + var reader = new FileReader(); + reader.onload = function(e) { + var p5file = new p5.File(theFile); + p5file.data = e.target.result; + callback(p5file); + }; + return reader; + }; + + p5.File._load = function(f, callback) { + // Text or data? + // This should likely be improved + if (/^text\//.test(f.type)) { + p5.File._createLoader(f, callback).readAsText(f); + } else if (!/^(video|audio)\//.test(f.type)) { + p5.File._createLoader(f, callback).readAsDataURL(f); + } else { + var file = new p5.File(f); + file.data = URL.createObjectURL(f); + callback(file); + } + }; +}); -- GitLab