UNPKG

p5

Version:

[![npm version](https://badge.fury.io/js/p5.svg)](https://www.npmjs.com/package/p5)

710 lines (693 loc) 24.6 kB
/** * @module Environment * @submodule Environment * @for p5 * @requires core */ function outputs(p5, fn){ /** * Creates a screen reader-accessible description of shapes on the canvas. * * `textOutput()` adds a general description, list of shapes, and * table of shapes to the web page. The general description includes the * canvas size, canvas color, and number of shapes. For example, * `Your output is a, 100 by 100 pixels, gray canvas containing the following 2 shapes:`. * * A list of shapes follows the general description. The list describes the * color, location, and area of each shape. For example, * `a red circle at middle covering 3% of the canvas`. Each shape can be * selected to get more details. * * `textOutput()` uses its table of shapes as a list. The table describes the * shape, color, location, coordinates and area. For example, * `red circle location = middle area = 3%`. This is different from * <a href="#/p5/gridOutput">gridOutput()</a>, which uses its table as a grid. * * The `display` parameter is optional. It determines how the description is * displayed. If `LABEL` is passed, as in `textOutput(LABEL)`, the description * will be visible in a div element next to the canvas. Using `LABEL` creates * unhelpful duplicates for screen readers. Only use `LABEL` during * development. If `FALLBACK` is passed, as in `textOutput(FALLBACK)`, the * description will only be visible to screen readers. This is the default * mode. * * Read * <a href="/learn/accessible-labels.html">Writing accessible canvas descriptions</a> * to learn more about making sketches accessible. * * @method textOutput * @param {(FALLBACK|LABEL)} [display] either FALLBACK or LABEL. * * @example * <div> * <code> * function setup() { * // Add the text description. * textOutput(); * * // Draw a couple of shapes. * background(200); * fill(255, 0, 0); * circle(20, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle and a blue square on a gray background.'); * } * </code> * </div> * * <div> * <code> * function setup() { * // Add the text description and * // display it for debugging. * textOutput(LABEL); * * // Draw a couple of shapes. * background(200); * fill(255, 0, 0); * circle(20, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle and a blue square on a gray background.'); * } * </code> * </div> * * <div> * <code> * * function setup(){ * createCanvas(100, 100); * } * * function draw() { * // Add the text description. * textOutput(); * * // Draw a moving circle. * background(200); * let x = frameCount * 0.1; * fill(255, 0, 0); * circle(x, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle moves from left to right above a blue square.'); * } * </code> * </div> * * <div> * <code> * * function setup(){ * createCanvas(100, 100); * } * * function draw() { * // Add the text description and * // display it for debugging. * textOutput(LABEL); * * // Draw a moving circle. * background(200); * let x = frameCount * 0.1; * fill(255, 0, 0); * circle(x, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle moves from left to right above a blue square.'); * } * </code> * </div> */ fn.textOutput = function(display) { // p5._validateParameters('textOutput', arguments); //if textOutput is already true if (this._accessibleOutputs.text) { return; } else { //make textOutput true this._accessibleOutputs.text = true; //create output for fallback this._createOutput('textOutput', 'Fallback'); if (display === this.LABEL) { //make textOutput label true this._accessibleOutputs.textLabel = true; //create output for label this._createOutput('textOutput', 'Label'); } } }; /** * Creates a screen reader-accessible description of shapes on the canvas. * * `gridOutput()` adds a general description, table of shapes, and list of * shapes to the web page. The general description includes the canvas size, * canvas color, and number of shapes. For example, * `gray canvas, 100 by 100 pixels, contains 2 shapes: 1 circle 1 square`. * * `gridOutput()` uses its table of shapes as a grid. Each shape in the grid * is placed in a cell whose row and column correspond to the shape's location * on the canvas. The grid cells describe the color and type of shape at that * location. For example, `red circle`. These descriptions can be selected * individually to get more details. This is different from * <a href="#/p5/textOutput">textOutput()</a>, which uses its table as a list. * * A list of shapes follows the table. The list describes the color, type, * location, and area of each shape. For example, * `red circle, location = middle, area = 3 %`. * * The `display` parameter is optional. It determines how the description is * displayed. If `LABEL` is passed, as in `gridOutput(LABEL)`, the description * will be visible in a div element next to the canvas. Using `LABEL` creates * unhelpful duplicates for screen readers. Only use `LABEL` during * development. If `FALLBACK` is passed, as in `gridOutput(FALLBACK)`, the * description will only be visible to screen readers. This is the default * mode. * * Read * <a href="/learn/accessible-labels.html">Writing accessible canvas descriptions</a> * to learn more about making sketches accessible. * * @method gridOutput * @param {(FALLBACK|LABEL)} [display] either FALLBACK or LABEL. * * @example * <div> * <code> * function setup() { * // Add the grid description. * gridOutput(); * * // Draw a couple of shapes. * background(200); * fill(255, 0, 0); * circle(20, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle and a blue square on a gray background.'); * } * </code> * </div> * * <div> * <code> * function setup() { * // Add the grid description and * // display it for debugging. * gridOutput(LABEL); * * // Draw a couple of shapes. * background(200); * fill(255, 0, 0); * circle(20, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle and a blue square on a gray background.'); * } * </code> * </div> * * <div> * <code> * * function setup() { * createCanvas(100, 100); * } * * function draw() { * // Add the grid description. * gridOutput(); * * // Draw a moving circle. * background(200); * let x = frameCount * 0.1; * fill(255, 0, 0); * circle(x, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle moves from left to right above a blue square.'); * } * </code> * </div> * * <div> * <code> * * function setup(){ * createCanvas(100, 100); * } * * function draw() { * // Add the grid description and * // display it for debugging. * gridOutput(LABEL); * * // Draw a moving circle. * background(200); * let x = frameCount * 0.1; * fill(255, 0, 0); * circle(x, 20, 20); * fill(0, 0, 255); * square(50, 50, 50); * * // Add a general description of the canvas. * describe('A red circle moves from left to right above a blue square.'); * } * </code> * </div> */ fn.gridOutput = function(display) { // p5._validateParameters('gridOutput', arguments); //if gridOutput is already true if (this._accessibleOutputs.grid) { return; } else { //make gridOutput true this._accessibleOutputs.grid = true; //create output for fallback this._createOutput('gridOutput', 'Fallback'); if (display === this.LABEL) { //make gridOutput label true this._accessibleOutputs.gridLabel = true; //create output for label this._createOutput('gridOutput', 'Label'); } } }; //helper function returns true when accessible outputs are true fn._addAccsOutput = function() { //if there are no accessible outputs create object with all false if (!this._accessibleOutputs) { this._accessibleOutputs = { text: false, grid: false, textLabel: false, gridLabel: false }; } return this._accessibleOutputs.grid || this._accessibleOutputs.text; }; //helper function that creates html structure for accessible outputs fn._createOutput = function(type, display) { let cnvId = this.canvas.id; //if there are no ingredients create object. this object stores data for the outputs if (!this.ingredients) { this.ingredients = { shapes: {}, colors: { background: 'white', fill: 'white', stroke: 'black' }, pShapes: '', pBackground: '' }; } //if there is no dummyDOM create it if (!this.dummyDOM) { this.dummyDOM = document.getElementById(cnvId).parentNode; } let cIdT, container, inner; let query = ''; if (display === 'Fallback') { cIdT = cnvId + type; container = cnvId + 'accessibleOutput'; if (!this.dummyDOM.querySelector(`#${container}`)) { //if there is no canvas description (see describe() and describeElement()) if (!this.dummyDOM.querySelector(`#${cnvId}_Description`)) { //create html structure inside of canvas this.dummyDOM.querySelector( `#${cnvId}` ).innerHTML = `<div id="${container}" role="region" aria-label="Canvas Outputs"></div>`; } else { //create html structure after canvas description container this.dummyDOM .querySelector(`#${cnvId}_Description`) .insertAdjacentHTML( 'afterend', `<div id="${container}" role="region" aria-label="Canvas Outputs"></div>` ); } } } else if (display === 'Label') { query = display; cIdT = cnvId + type + display; container = cnvId + 'accessibleOutput' + display; if (!this.dummyDOM.querySelector(`#${container}`)) { //if there is no canvas description label (see describe() and describeElement()) if (!this.dummyDOM.querySelector(`#${cnvId}_Label`)) { //create html structure adjacent to canvas this.dummyDOM .querySelector(`#${cnvId}`) .insertAdjacentHTML('afterend', `<div id="${container}"></div>`); } else { //create html structure after canvas label this.dummyDOM .querySelector(`#${cnvId}_Label`) .insertAdjacentHTML('afterend', `<div id="${container}"></div>`); } } } //create an object to store the latest output. this object is used in _updateTextOutput() and _updateGridOutput() this._accessibleOutputs[cIdT] = {}; if (type === 'textOutput') { query = `#${cnvId}gridOutput${query}`; //query is used to check if gridOutput already exists inner = `<div id="${cIdT}">Text Output<div id="${cIdT}Summary" aria-label="text output summary"><p id="${cIdT}_summary"></p><ul id="${cIdT}_list"></ul></div><table id="${cIdT}_shapeDetails" summary="text output shape details"></table></div>`; //if gridOutput already exists if (this.dummyDOM.querySelector(query)) { //create textOutput before gridOutput this.dummyDOM .querySelector(query) .insertAdjacentHTML('beforebegin', inner); } else { //create output inside of container this.dummyDOM.querySelector(`#${container}`).innerHTML = inner; } //store output html elements this._accessibleOutputs[cIdT].list = this.dummyDOM.querySelector( `#${cIdT}_list` ); } else if (type === 'gridOutput') { query = `#${cnvId}textOutput${query}`; //query is used to check if textOutput already exists inner = `<div id="${cIdT}">Grid Output<p id="${cIdT}_summary" aria-label="grid output summary"><table id="${cIdT}_map" summary="grid output content"></table><ul id="${cIdT}_shapeDetails" aria-label="grid output shape details"></ul></div>`; //if textOutput already exists if (this.dummyDOM.querySelector(query)) { //create gridOutput after textOutput this.dummyDOM.querySelector(query).insertAdjacentHTML('afterend', inner); } else { //create output inside of container this.dummyDOM.querySelector(`#${container}`).innerHTML = inner; } //store output html elements this._accessibleOutputs[cIdT].map = this.dummyDOM.querySelector( `#${cIdT}_map` ); } this._accessibleOutputs[cIdT].shapeDetails = this.dummyDOM.querySelector( `#${cIdT}_shapeDetails` ); this._accessibleOutputs[cIdT].summary = this.dummyDOM.querySelector( `#${cIdT}_summary` ); }; //this function is called at the end of setup and draw if using //accessibleOutputs and calls update functions of outputs fn._updateAccsOutput = function() { let cnvId = this.canvas.id; //if the shapes are not the same as before if ( JSON.stringify(this.ingredients.shapes) !== this.ingredients.pShapes || this.ingredients.colors.background !== this.ingredients.pBackground ) { //save current shapes as string in pShapes this.ingredients.pShapes = JSON.stringify(this.ingredients.shapes); if (this._accessibleOutputs.text) { this._updateTextOutput(cnvId + 'textOutput'); } if (this._accessibleOutputs.grid) { this._updateGridOutput(cnvId + 'gridOutput'); } if (this._accessibleOutputs.textLabel) { this._updateTextOutput(cnvId + 'textOutputLabel'); } if (this._accessibleOutputs.gridLabel) { this._updateGridOutput(cnvId + 'gridOutputLabel'); } } }; //helper function that resets all ingredients when background is called //and saves background color name fn._accsBackground = function(args) { //save current shapes as string in pShapes this.ingredients.pShapes = JSON.stringify(this.ingredients.shapes); this.ingredients.pBackground = this.ingredients.colors.background; //empty shapes JSON this.ingredients.shapes = {}; //update background different if (this.ingredients.colors.backgroundRGBA !== args) { this.ingredients.colors.backgroundRGBA = args; this.ingredients.colors.background = this._rgbColorName(args); } }; //helper function that gets fill and stroke of shapes fn._accsCanvasColors = function(f, args) { if (f === 'fill') { //update fill different if (this.ingredients.colors.fillRGBA !== args) { this.ingredients.colors.fillRGBA = args; this.ingredients.colors.fill = this._rgbColorName(args); } } else if (f === 'stroke') { //update stroke if different if (this.ingredients.colors.strokeRGBA !== args) { this.ingredients.colors.strokeRGBA = args; this.ingredients.colors.stroke = this._rgbColorName(args); } } }; //builds ingredients.shapes used for building outputs fn._accsOutput = function(f, args) { if (f === 'ellipse' && args[2] === args[3]) { f = 'circle'; } else if (f === 'rectangle' && args[2] === args[3]) { f = 'square'; } let include = {}; let add = true; let middle = _getMiddle(f, args); if (f === 'line') { //make color stroke include.color = this.ingredients.colors.stroke; //get lenght include.length = Math.round( Math.hypot(args[2] - args[0], args[3] - args[1]) ); //get position of end points let p1 = this._getPos(args[0], [1]); let p2 = this._getPos(args[2], [3]); include.loc = _canvasLocator(middle, this.width, this.height); if (p1 === p2) { include.pos = `at ${p1}`; } else { include.pos = `from ${p1} to ${p2}`; } } else { if (f === 'point') { //make color stroke include.color = this.ingredients.colors.stroke; } else { //make color fill include.color = this.ingredients.colors.fill; //get area of shape include.area = this._getArea(f, args); } //get middle of shapes //calculate position using middle of shape include.pos = this._getPos(...middle); //calculate location using middle of shape include.loc = _canvasLocator(middle, this.width, this.height); } //if it is the first time this shape is created if (!this.ingredients.shapes[f]) { this.ingredients.shapes[f] = [include]; //if other shapes of this type have been created } else if (this.ingredients.shapes[f] !== [include]) { //for every shape of this type for (let y in this.ingredients.shapes[f]) { //compare it with current shape and if it already exists make add false if ( JSON.stringify(this.ingredients.shapes[f][y]) === JSON.stringify(include) ) { add = false; } } //add shape by pushing it to the end if (add === true) { this.ingredients.shapes[f].push(include); } } }; //gets middle point / centroid of shape function _getMiddle(f, args) { let x, y; if ( f === 'rectangle' || f === 'ellipse' || f === 'arc' || f === 'circle' || f === 'square' ) { x = Math.round(args[0] + args[2] / 2); y = Math.round(args[1] + args[3] / 2); } else if (f === 'triangle') { x = (args[0] + args[2] + args[4]) / 3; y = (args[1] + args[3] + args[5]) / 3; } else if (f === 'quadrilateral') { x = (args[0] + args[2] + args[4] + args[6]) / 4; y = (args[1] + args[3] + args[5] + args[7]) / 4; } else if (f === 'line') { x = (args[0] + args[2]) / 2; y = (args[1] + args[3]) / 2; } else { x = args[0]; y = args[1]; } return [x, y]; } //gets position of shape in the canvas fn._getPos = function (x, y) { const { x: transformedX, y: transformedY } = this.worldToScreen(new p5.Vector(x, y)); const canvasWidth = this.width; const canvasHeight = this.height; if (transformedX < 0.4 * canvasWidth) { if (transformedY < 0.4 * canvasHeight) { return 'top left'; } else if (transformedY > 0.6 * canvasHeight) { return 'bottom left'; } else { return 'mid left'; } } else if (transformedX > 0.6 * canvasWidth) { if (transformedY < 0.4 * canvasHeight) { return 'top right'; } else if (transformedY > 0.6 * canvasHeight) { return 'bottom right'; } else { return 'mid right'; } } else { if (transformedY < 0.4 * canvasHeight) { return 'top middle'; } else if (transformedY > 0.6 * canvasHeight) { return 'bottom middle'; } else { return 'middle'; } } }; //locates shape in a 10*10 grid function _canvasLocator(args, canvasWidth, canvasHeight) { const noRows = 10; const noCols = 10; let locX = Math.floor(args[0] / canvasWidth * noRows); let locY = Math.floor(args[1] / canvasHeight * noCols); if (locX === noRows) { locX = locX - 1; } if (locY === noCols) { locY = locY - 1; } return { locX, locY }; } //calculates area of shape fn._getArea = function (objectType, shapeArgs) { let objectArea = 0; if (objectType === 'arc') { // area of full ellipse = PI * horizontal radius * vertical radius. // therefore, area of arc = difference bet. arc's start and end radians * horizontal radius * vertical radius. // the below expression is adjusted for negative values and differences in arc's start and end radians over PI*2 const arcSizeInRadians = ((shapeArgs[5] - shapeArgs[4]) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2); objectArea = arcSizeInRadians * shapeArgs[2] * shapeArgs[3] / 8; if (shapeArgs[6] === 'open' || shapeArgs[6] === 'chord') { // when the arc's mode is OPEN or CHORD, we need to account for the area of the triangle that is formed to close the arc // (Ax( By − Cy) + Bx(Cy − Ay) + Cx(Ay − By ) )/2 const Ax = shapeArgs[0]; const Ay = shapeArgs[1]; const Bx = shapeArgs[0] + shapeArgs[2] / 2 * Math.cos(shapeArgs[4]).toFixed(2); const By = shapeArgs[1] + shapeArgs[3] / 2 * Math.sin(shapeArgs[4]).toFixed(2); const Cx = shapeArgs[0] + shapeArgs[2] / 2 * Math.cos(shapeArgs[5]).toFixed(2); const Cy = shapeArgs[1] + shapeArgs[3] / 2 * Math.sin(shapeArgs[5]).toFixed(2); const areaOfExtraTriangle = Math.abs(Ax * (By - Cy) + Bx * (Cy - Ay) + Cx * (Ay - By)) / 2; if (arcSizeInRadians > Math.PI) { objectArea = objectArea + areaOfExtraTriangle; } else { objectArea = objectArea - areaOfExtraTriangle; } } } else if (objectType === 'ellipse' || objectType === 'circle') { objectArea = 3.14 * shapeArgs[2] / 2 * shapeArgs[3] / 2; } else if (objectType === 'line') { objectArea = 0; } else if (objectType === 'point') { objectArea = 0; } else if (objectType === 'quadrilateral') { // ((x4+x1)*(y4-y1)+(x1+x2)*(y1-y2)+(x2+x3)*(y2-y3)+(x3+x4)*(y3-y4))/2 objectArea = Math.abs( (shapeArgs[6] + shapeArgs[0]) * (shapeArgs[7] - shapeArgs[1]) + (shapeArgs[0] + shapeArgs[2]) * (shapeArgs[1] - shapeArgs[3]) + (shapeArgs[2] + shapeArgs[4]) * (shapeArgs[3] - shapeArgs[5]) + (shapeArgs[4] + shapeArgs[6]) * (shapeArgs[5] - shapeArgs[7]) ) / 2; } else if (objectType === 'rectangle' || objectType === 'square') { objectArea = shapeArgs[2] * shapeArgs[3]; } else if (objectType === 'triangle') { objectArea = Math.abs( shapeArgs[0] * (shapeArgs[3] - shapeArgs[5]) + shapeArgs[2] * (shapeArgs[5] - shapeArgs[1]) + shapeArgs[4] * (shapeArgs[1] - shapeArgs[3]) ) / 2; // (Ax( By − Cy) + Bx(Cy − Ay) + Cx(Ay − By ))/2 } // Store the positions of the canvas corners const canvasWidth = this.width * this._renderer._pixelDensity; const canvasHeight = this.height * this._renderer._pixelDensity; const canvasCorners = [ new DOMPoint(0, 0), new DOMPoint(canvasWidth, 0), new DOMPoint(canvasWidth, canvasHeight), new DOMPoint(0, canvasHeight) ]; // Apply the inverse of the current transformations to the canvas corners const currentTransform = this._renderer.isP3D ? new DOMMatrix(this._renderer.uMVMatrix.mat4) : this.drawingContext.getTransform(); const invertedTransform = currentTransform.inverse(); const tc = canvasCorners.map( corner => corner.matrixTransform(invertedTransform) ); /* Use same shoelace formula used for quad area (above) to calculate the area of the canvas with inverted transformation applied */ const transformedCanvasArea = Math.abs( (tc[3].x + tc[0].x) * (tc[3].y - tc[0].y) + (tc[0].x + tc[1].x) * (tc[0].y - tc[1].y) + (tc[1].x + tc[2].x) * (tc[1].y - tc[2].y)+ (tc[2].x + tc[3].x) * (tc[2].y - tc[3].y) ) / 2; /* Compare area of shape (minus transformations) to area of canvas with inverted transformation applied. Return percentage */ const untransformedArea = Math.round( objectArea * 100 / (transformedCanvasArea) ); return untransformedArea; }; } if(typeof p5 !== 'undefined'){ outputs(p5, p5.prototype); } export { outputs as default };