diff --git a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.md b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.md index ebab8deb08..7224403ac3 100644 --- a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.md +++ b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.md @@ -15,6 +15,7 @@ To check any option, replace the "[ ]" with a "[x]". Be sure to check out how it #### Most appropriate sub-area of p5.js? +- [ ] Accessibility (Web Accessibility) - [ ] Color - [ ] Core/Environment/Rendering - [ ] Data diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 07108882f4..6fd139797a 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -24,6 +24,7 @@ To check any option, replace the "[ ]" with a "[x]". Be sure to check out how it #### Most appropriate sub-area of p5.js? +- [ ] Accessibility (Web Accessibility) - [ ] Color - [ ] Core/Environment/Rendering - [ ] Data diff --git a/.github/ISSUE_TEMPLATE/found-a-bug.md b/.github/ISSUE_TEMPLATE/found-a-bug.md index abf11fcb4a..1dc6ceb85c 100644 --- a/.github/ISSUE_TEMPLATE/found-a-bug.md +++ b/.github/ISSUE_TEMPLATE/found-a-bug.md @@ -15,6 +15,7 @@ To check any option, replace the "[ ]" with a "[x]". Be sure to check out how it #### Most appropriate sub-area of p5.js? +- [ ] Accessibility (Web Accessibility) - [ ] Color - [ ] Core/Environment/Rendering - [ ] Data diff --git a/contributor_docs/README.md b/contributor_docs/README.md index 35e98bd5d5..5b58034e04 100644 --- a/contributor_docs/README.md +++ b/contributor_docs/README.md @@ -16,7 +16,6 @@ The overarching p5.js project includes some repositories other than this one: - [p5.js-website](https://github.com/processing/p5.js-website) This repository contains most of the code for the [p5.js website](http://p5js.org), with the exception of the reference manual. It is maintained by [Lauren Lee McCarthy](https://github.com/lmccart). - [p5.js-sound](https://github.com/processing/p5.js-sound) This repository contains the p5.sound.js library. It is maintained by [Jason Sigal](https://github.com/therewasaguy). - [p5.js-web-editor](https://github.com/processing/p5.js-web-editor) This repository contains the source code for the [p5.js web editor](https://editor.p5js.org). It is maintained by [Cassie Tarakajian](https://github.com/catarak). Note that the older [p5.js editor](https://github.com/processing/p5.js-editor) is now deprecated. -- [p5.accessibility](https://github.com/processing/p5.accessibility) A library that makes the p5 canvas more accessible to people who are blind and visually impaired. diff --git a/contributor_docs/project_wrapups/README.md b/contributor_docs/project_wrapups/README.md index 0bcb95b65e..f8efef3724 100644 --- a/contributor_docs/project_wrapups/README.md +++ b/contributor_docs/project_wrapups/README.md @@ -3,6 +3,8 @@ This folder contains wrapup reports for projects from p5.js related [Google Summ *Note for contributors: Embedded images and media are welcome. Please host these files externally rather than placing in this repo to avoid adding growing the repository filesize too much.* +### Google Summer of Code 2020 +* [p5.js accessibility and canvas descriptions](https://github.com/processing/p5.js/blob/main/contributor_docs/project_wrapups/luismn_gsoc_2020.md) by Luis Morales-Navarro, 2020 ### Google Summer of Code 2019 * [Search Bar for Sketches in the p5.js Web Editor](https://github.com/processing/p5.js/blob/main/contributor_docs/project_wrapups/rachellim_gsoc_2019.md) by Rachel Lim, 2019 diff --git a/contributor_docs/project_wrapups/luismn_gsoc_2020.md b/contributor_docs/project_wrapups/luismn_gsoc_2020.md new file mode 100644 index 0000000000..c5a5e6da0d --- /dev/null +++ b/contributor_docs/project_wrapups/luismn_gsoc_2020.md @@ -0,0 +1,53 @@ +# p5.js accessibility and canvas descriptions +GSoC 2020 | [Luis Morales-Navarro](https://luismn.com/) + +### Overview: +During this Google Summer of Code, I worked with [Kate Hollenbach](https://github.com/kjhollen) +to improve the accessibility features of p5.js. We focused on merging the +text output and table output functionalities of [p5.accessibility](https://github.com/processing/p5.accessibility) +into p5.js and created functions that support p5.js users in writing their own screen reader accessible canvas descriptions. + +### Background: +#### p5.js and Accessibility: from an editor feature to an add-on to the library +The work done during this summer is part of the project's [efforts to make p5.js more accessible for persons with dissabilities](https://contributors-zine.p5js.org/#reflection-claire-kearney-volpe). +Early work by Claire Kearney-Volpe, Taeyoon Choi, and Atul Varma identified the need to make p5.js sketches +and the canvas accessible to screen readers and people who are blind. I met Claire in late 2016 when +they were working with Mathura Govindarajan to add accessibility features to the p5.js editor. I joined them +and together with the support of dedicated contributors and advisors (including Cassie Tarajakan, Lauren McCarthy, +Josh Mielle, Sina Bahram, and Chancey Fleet) we implemented three accessible canvas outputs (a text output, a grid +output and a sound output) on the alpha editor. + +Later on, through a 2018 Processing Foundation Fellowship Claire, Mathura and I developed p5.accessibility.js a p5.js add-on. +p5.accessibility.js (developed with contributions from Antonio Guimaraes, Elizabeth G Betts, Mithru Vigneshwara, and Yossi Spira) +helped us bring the work we had done with accessible outputs in the editor to any p5.js sketch that included the add-on. +However, the add-on was still an add-on that required users to include an extra file and edit their html. + +At the 2019 p5.js Contributors Conference, as a community, we reinforced the project's commitment to access and inclussion. +Together with Claire, Sina, Lauren, Kate, Olivia McKayla Ross and Evelyn Masso we outlined the pathway forward. +Among short-term actions, we identified the need for functions that allow users to write their own descriptions +and the importance of merging the add-on into the p5.js library. + +### Contributions: +During the course of Summer of Code, my work focused on creating library generated screen reader accessible outputs +for basic shapes on the canvas and functions to support user-generated screen reader accessible descriptions of canvas content. +I worked on the following PRs: +- [Add describe() and describeElement() #4654](https://github.com/processing/p5.js/pull/4654): This PR adds the functions describe() and describeElement(), tests for these functions, documentation and examples. +- [Merge Accessibility Add-On into p5.js #4703](https://github.com/processing/p5.js/pull/4703): This PR adds the functions textOutput() and gridOutput(), helper functions to create and update, the outputs and tests, documentation and examples. At first the plan was to update the add-on and prepare it for merging it with p5.js in the near future. However, we realized it was more time effective to recreate the functionality of the text output and grid output in p5.js than upgrading the add-on which relied on ["monkey patching," entities and interceptors](https://medium.com/processing-foundation/making-p5-js-accessible-e2ce366e05a0). Now, the outputs are fully integrated to the library. + +More information on how these accessibility features work is available in the [web accessibility contributor docs](https://github.com/processing/p5.js/blob/main/contributor_docs/web_accessibility.md). + +### Future +- There is a lot of work that can be done to improve the accessibility of p5.js sketches. In the [Web accessibility next steps conversation #4721 Issue](https://github.com/processing/p5.js/issues/4721) we have outlined some ideas and questions. +- The work done during the summer focused on code and code issues but it is important to iteratively test these features with members of the community, particularly novices and learners who are blind. It is also important to create more resources for learning and teaching that support accessibility. +- Immediate next steps include: + - A tutorial on how to describe things on the canvas. + - Changes in the way screen-reader descriptions are created in the reference. Using the describe() function instead of relying on @alt + - Maybe adding describe() to the templates on the website and editor + - Upgrading the tutorial on using p5 with a screen reader + - Changing the way the accessibility settings work on the editor + +### Acknowledgements +I am grateful to Kate Hollenbach for their guidance, feedback and assistance, to Lauren McCarthy for their feedback and to Claire Kearney-Volpe for helping me come up with this project. Thanks to Sina Bahram for their input —our conversations at 2019 p5.js Contributors Conference inspired the describe() and describeElement() functions—, and to Akshay Padte for their advice on unit testing. This GSoC project would not have been possible without Chancey Fleet and Claire (who started thinking of ways to make p5.js sketches screen reader accessible in late 2015), the work of Mathura Govindarajan, and of many other contributors and supporters in the p5.js community. + +:heart: + diff --git a/contributor_docs/sidebar.md b/contributor_docs/sidebar.md index 5884b088b4..d365f9014c 100644 --- a/contributor_docs/sidebar.md +++ b/contributor_docs/sidebar.md @@ -21,3 +21,4 @@ - [WebGL Architecture](webgl_mode_architecture.md) - [Supported Browsers](supported_browsers.md) - [Custom Builds](custom_p5_build.md) + - [Web Accessibility](web_accessibility.md) diff --git a/contributor_docs/web_accessibility.md b/contributor_docs/web_accessibility.md new file mode 100644 index 0000000000..bf4dbcb202 --- /dev/null +++ b/contributor_docs/web_accessibility.md @@ -0,0 +1,94 @@ +# p5.js Web Accessibility + +This document describes the structure of the web accessibility features of p5.js for contributors and maintainers—and any other interested parties. If you're interested in making your sketches [screen reader](https://en.wikipedia.org/wiki/Screen_reader) accessible, visit the [tutorial](https://p5js.org/learn) or if you want to use p5.js with a screen reader visit the [Using p5.js with a Screen Reader tutorial](https://p5js.org/learn/p5-screen-reader.html). + +## Overview + +Because the canvas HTML element is a bitmap and does not provide any screen reader accessible information on the shapes drawn on it, p5.js has several functions that make the canvas more accessible to screen readers. + +Currently, p5.js supports library generated screen reader accessible outputs for basic shapes on the canvas (with `textOutput()` and `gridOutput()`) and user-generated screen reader accessible descriptions of canvas content (with `describe()` and `describeElement()`). + +## Library generated accessible outputs for basic shapes + +Supported accessible outputs for basic shapes include text and grid outputs. + +`textOutput()` creates a screen reader accessible output that describes the shapes present on the canvas. The general description of the canvas includes canvas size, canvas color, and number of elements in the canvas (example: 'Your output is a, 400 by 400 pixels, lavender blue canvas containing the following 4 shapes:'). This description is followed by a list of shapes where the color, position, and area of each shape are described (example: "orange ellipse at top left covering 1% of the canvas"). Each element can be selected to get more details. A table of elements is also provided. In this table, shape, color, location, coordinates and area are described (example: "orange ellipse location=top left area=2"). + +`gridOutput()` lays out the content of the canvas in the form of a grid (html table) based on the spatial location of each shape. A brief description of the canvas is available before the table output. This description includes: color of the background, size of the canvas, number of objects, and object types (example: "lavender blue canvas is 200 by 200 and contains 4 objects - 3 ellipses 1 rectangle"). The grid describes the content spatially, each element is placed on a cell of the table depending on its position. Within each cell an element the color and type of shape of that element are available (example: "orange ellipse"). These descriptions can be selected individually to get more details. A list of elements where shape, color, location, and area are described (example: "orange ellipse location=top left area=1%") is also available. + +If a user passes `LABEL` as a parameter in either of these functions, an additional div with the output adjacent to the canvas is created, this is useful for non-screen reader users that might want to display the output outside of the canvas' sub DOM as they code. However, using `LABEL` will create unnecessary redundancy for screen reader users. We recommend using `LABEL` only as part of the development process of a sketch and removing it before publishing or sharing your sketch with screen reader users. + +### Outputs structure +Although `textOutput()` and `gridOutput()` are located in [src/accessibility/outputs.js](https://github.com/processing/p5.js/blob/main/src/accessibility/outputs.js), the outputs are created and updated using functions distributed across the library. This section details the different functions that support the accessible outputs. + +#### outputs.js +[src/accessibility/outputs.js](https://github.com/processing/p5.js/blob/main/src/accessibility/outputs.js) includes the core functions that create the accessible outputs: +* `textOutput()`: This function activates the text output by setting `this._accessibleOutputs.text` to `true` and calling `_createOutput('textOutput', 'Fallback')`. If `LABEL` is passed as a parameter the function also activates the text output label by setting `this._accessibleOutputs.textLabel` as `true` and calls `_createOutput('textOutput', 'Label')` for the label. +* `gridOutput()`: This function activates the grid output by setting `this._accessibleOutputs.grid` to `true` and calling `_createOutput('gridOutput', 'Fallback')`. If `LABEL` is passed as a parameter the function also activates the grid output label by setting `this._accessibleOutputs.textLabel` as `true` and calls `_createOutput('gridOutput', 'Label')` for the label. +* `_createOutput()`: This function creates the HTML structure for all accessible outputs. Depending on the type and display of the outputs the HTML structure created varies. The function also initializes `this.ingredients` which stores all the data for the outputs including: shapes, colors, and pShapes (which stores a string of the previous shapes of the canvas). It also creates `this.dummyDOM` if it doesn't exist. `this.dummyDOM` stores the HTMLCollection of DOM elements inside of ``. +* `_updateAccsOutput()`: Is called at the end of `setup()` and `draw()` if using accessibleOutputs. if `this.ingredients` is different than the current outputs, this function calls the update functions of outputs(`_updateGridOutput` and `_updateTextOutput`). Calling this function only at the end of `setup()` and `draw()` as well as only calling `_updateGridOutput` and `_updateTextOutput` only when the ingredients are different helps avoid overwhelming the screen reader. +* `_addAccsOutput()`: This function returns true when accessibleOutputs are true. +* `_accsBackground()`: Is called at the end of `background()`. It resets `this.ingredients.shapes` and if the color of the background is different than before it calls `_rgbColorName()` to get the name of the color and store it in `this.ingredients.colors.background` +* `_accsCanvasColors()`: Is called at the end of fill() and stroke(). This function updates the fill and stroke colors by saving them in `this.ingredients.colors.fill` and `this.ingredients.colors.stroke`. It also calls `_rgbColorName()` to get the names of the colors. +* `_accsOutput()`: Builds `this.ingredients.shapes` which includes all the shapes that are used for creating the outputs. This function is called at the end of the basic shape functions (see accessible output beyond src/accessibility). Depending on the shape that calls it, `_accsOutput()` may call helper functions to gather all the information about that shape that will be needed to create the outputs. These functions, which are not part of the prototype, include: + * `_getMiddle()`: Returns the middle point or centroid of rectangles, arcs, ellipses, triangles, and quadrilaterals. + * `_getPos()`: Returns the position of a shape on the canvas (e.g.: 'top left', 'mid right'). + * `_canvasLocator()`: Returns location of the shape on a 10*10 grid mapped to the canvas. + * `_getArea()`: Returns the area of the shape as a percentage of the canvas' total area. + +When `this._accessibleOutputs.text` or `this._accessibleOutputs.text` are `true` several functions across the p5.js library call functions in output.js: +* `_accsOutput()` is called in: + * `p5.prototype.triangle()` + * `p5.prototype._renderRect()` + * `p5.prototype.quad()` + * `pp5.prototype.point()` + * `p5.prototype.line()` + * `p5.prototype._renderEllipse()` + * `p5.prototype.arc()` +* `_updateAccsOutput()` is called in: + * `p5.prototype.redraw()` + * `p5.prototype.resizeCanvas()` + * `this._setup` +* `_accsCanvasColors()` is called in: + * `p5.Renderer2D.prototype.stroke()` + * `p5.Renderer2D.prototype.fill()` +* `_accsBackground()` is called in: + * `p5.Renderer2D.prototype.background()` + +#### textOutput.js +[src/accessibility/textOutput.js](https://github.com/processing/p5.js/blob/main/src/accessibility/textOutput.js) contains all functions that update the text output. The main function in this file is `_updateTextOutput()` which is called by `_updateAccsOutput()` in [src/accessibility/outputs.js](https://github.com/processing/p5.js/blob/main/src/accessibility/outputs.js) when `this._accessibleOutputs.text` or `this._accessibleOutputs.textLabel` are `true.` + +`_updateTextOutput()` uses `this.ingredients` to build the content of the text output and text output label which include a summary, a list of shapes, and a table of shapes details. If these are different from the current outputs it updates them. Building the output content is supported by several helper functions in the file that are not part of the prototype: +* `_textSummary()`: Builds the content of the text output summary. +* `_shapeDetails()`: Builds the text output table that contains shape details. +* `_shapeList()`: Builds the list of shapes of the text output. + +#### gridOutput.js +[src/accessibility/gridOutput.js](https://github.com/processing/p5.js/blob/main/src/accessibility/gridOutput.js) contains all functions that update the grid output. The main function in this file is `_updateGridOutput()` which is called by `_updateAccsOutput()` in [src/accessibility/outputs.js](https://github.com/processing/p5.js/blob/main/src/accessibility/outputs.js) when `this._accessibleOutputs.grid` or `this._accessibleOutputs.gridLabel` are `true.` + +`_updateGridOutput()` uses `this.ingredients` to build the content of the grid output and grid output label which include a summary, a grid that maps the location of shapes and a list of shapes. If these are different from the current outputs it updates them. Building the output content is supported by several helper functions in the file that are not part of the prototype: +* `_gridSummary()`: Builds the content of the grid output summary. +* `_gridMap()`: Builds a grid that maps the location of shapes on the canvas. +* `_gridShapeDetails()`: Builds the list of shapes of the grid output, each line of the list includes details about the shape. + +#### color_namer.js +When creating screen reader accessible outputs, naming the colors used in the canvas is important. [src/accessibility/color_namer.js](https://github.com/processing/p5.js/blob/main/src/accessibility/color_namer.js) contains `_rgbColorName()` a function that receives rgba values and returns a color name. This function is called by `_accsBackground()` and `_accsCanvasColors` in [src/accessibility/outputs.js](https://github.com/processing/p5.js/blob/main/src/accessibility/outputs.js). + +`_rgbColorName()` uses `color_conversion._rgbaToHSBA()` to get the hsv values of the color and then uses `_calculateColor()` to get the color name. The function `_calculateColor()` in this file comes from [colorNamer.js](https://github.com/MathuraMG/color-namer) which was developed as part of a [2018 Processing Foundation fellowship](https://medium.com/processing-foundation/making-p5-js-accessible-e2ce366e05a0) and in consultation with blind screen reader expert users. This function returns color names by comparing hsv values to those stored in the `colorLookUp` array. The function should be updated as some shades of gray are not named correctly. When updating it, it is also important to ensure contributor readability by including comments that explain what each line of code does. + +## User-generated accessible canvas descriptions + +### describe() +The `describe()` function creates a screen reader accessible description for the canvas. The first parameter should be a string with a description of the canvas. The second parameter is optional. If specified, it determines how the description is displayed. All descriptions become part of the sub DOM of the canvas element. If a user passes `LABEL` as a second parameter, an additional div with the description adjacent to the canvas is created. + +`describe()` is supported by several functions in [src/accessibility/describe.js](https://github.com/processing/p5.js/blob/main/src/accessibility/describe.js): +* `_descriptionText()`: Checks that text is not `LABEL` or `FALLBACK` and ensures text ends with a punctuation mark. If the text does not end with '.', ',', ';', '?', '!', this function adds a '.' at the end of the string. Returns text. +* `_describeHTML()`: Creates fallback HTML structure for the canvas. If the second parameter of `describe()` is `LABEL`, this function creates a div adjacent to the canvas element for the description text. + +### describeElement() +The `describeElement()` function creates a screen reader accessible description for sketch elements or groups of shapes that create meaning together. The first parameter should be a string with the name of the element, the second parameter should be a string with the description of the element. The third parameter is optional. If specified, it determines how the description is displayed. All element descriptions become part of the sub DOM of the canvas element. If a user passes `LABEL` as a third parameter, an additional div with the element description adjacent to the canvas is created. + +`describeElement()` is supported by several functions in [src/accessibility/describe.js](https://github.com/processing/p5.js/blob/main/src/accessibility/describe.js): +* `_elementName()`: Checks that element name is not `LABEL` or `FALLBACK` and ensures text ends with a colon. Returns element name. +* `_descriptionText()`: Checks that text is not `LABEL` or `FALLBACK` and ensures text ends with a punctuation mark. If the text does not end with '.', ',', ';', '?', '!', this function adds a '.' at the end of the string. Returns text. +* `_describeElementHTML()`: Creates fallback HTML structure for the canvas. When the second parameter of `describeElement()` is `LABEL`, this function creates a div adjacent to the canvas element for the descriptions. diff --git a/src/accessibility/color_namer.js b/src/accessibility/color_namer.js new file mode 100644 index 0000000000..4509280e5b --- /dev/null +++ b/src/accessibility/color_namer.js @@ -0,0 +1,715 @@ +/** + * @module Environment + * @submodule Environment + * @for p5 + * @requires core + */ + +import p5 from '../core/main'; +import color_conversion from '../color/color_conversion'; + +//stores the original hsb values +let originalHSB; + +//stores values for color name exceptions +const colorExceptions = [ + { + h: 0, + s: 0, + b: 0.8275, + name: 'gray' + }, + { + h: 0, + s: 0, + b: 0.8627, + name: 'gray' + }, + { + h: 0, + s: 0, + b: 0.7529, + name: 'gray' + }, + { + h: 0.0167, + s: 0.1176, + b: 1, + name: 'light pink' + } +]; + +//stores values for color names +const colorLookUp = [ + { + h: 0, + s: 0, + b: 0, + name: 'black' + }, + { + h: 0, + s: 0, + b: 0.5, + name: 'gray' + }, + { + h: 0, + s: 0, + b: 1, + name: 'white' + }, + { + h: 0, + s: 0.5, + b: 0.5, + name: 'dark maroon' + }, + { + h: 0, + s: 0.5, + b: 1, + name: 'salmon pink' + }, + { + h: 0, + s: 1, + b: 0, + name: 'black' + }, + { + h: 0, + s: 1, + b: 0.5, + name: 'dark red' + }, + { + h: 0, + s: 1, + b: 1, + name: 'red' + }, + { + h: 5, + s: 0, + b: 1, + name: 'very light peach' + }, + { + h: 5, + s: 0.5, + b: 0.5, + name: 'brown' + }, + { + h: 5, + s: 0.5, + b: 1, + name: 'peach' + }, + { + h: 5, + s: 1, + b: 0.5, + name: 'brick red' + }, + { + h: 5, + s: 1, + b: 1, + name: 'crimson' + }, + { + h: 10, + s: 0, + b: 1, + name: 'light peach' + }, + { + h: 10, + s: 0.5, + b: 0.5, + name: 'brown' + }, + { + h: 10, + s: 0.5, + b: 1, + name: 'light orange' + }, + { + h: 10, + s: 1, + b: 0.5, + name: 'brown' + }, + { + h: 10, + s: 1, + b: 1, + name: 'orange' + }, + { + h: 15, + s: 0, + b: 1, + name: 'very light yellow' + }, + { + h: 15, + s: 0.5, + b: 0.5, + name: 'olive green' + }, + { + h: 15, + s: 0.5, + b: 1, + name: 'light yellow' + }, + { + h: 15, + s: 1, + b: 0, + name: 'dark olive green' + }, + { + h: 15, + s: 1, + b: 0.5, + name: 'olive green' + }, + { + h: 15, + s: 1, + b: 1, + name: 'yellow' + }, + { + h: 20, + s: 0, + b: 1, + name: 'very light yellow' + }, + { + h: 20, + s: 0.5, + b: 0.5, + name: 'olive green' + }, + { + h: 20, + s: 0.5, + b: 1, + name: 'light yellow green' + }, + { + h: 20, + s: 1, + b: 0, + name: 'dark olive green' + }, + { + h: 20, + s: 1, + b: 0.5, + name: 'dark yellow green' + }, + { + h: 20, + s: 1, + b: 1, + name: 'yellow green' + }, + { + h: 25, + s: 0.5, + b: 0.5, + name: 'dark yellow green' + }, + { + h: 25, + s: 0.5, + b: 1, + name: 'light green' + }, + { + h: 25, + s: 1, + b: 0.5, + name: 'dark green' + }, + { + h: 25, + s: 1, + b: 1, + name: 'green' + }, + { + h: 30, + s: 0.5, + b: 1, + name: 'light green' + }, + { + h: 30, + s: 1, + b: 0.5, + name: 'dark green' + }, + { + h: 30, + s: 1, + b: 1, + name: 'green' + }, + { + h: 35, + s: 0, + b: 0.5, + name: 'light green' + }, + { + h: 35, + s: 0, + b: 1, + name: 'very light green' + }, + { + h: 35, + s: 0.5, + b: 0.5, + name: 'dark green' + }, + { + h: 35, + s: 0.5, + b: 1, + name: 'light green' + }, + { + h: 35, + s: 1, + b: 0, + name: 'very dark green' + }, + { + h: 35, + s: 1, + b: 0.5, + name: 'dark green' + }, + { + h: 35, + s: 1, + b: 1, + name: 'green' + }, + { + h: 40, + s: 0, + b: 1, + name: 'very light green' + }, + { + h: 40, + s: 0.5, + b: 0.5, + name: 'dark green' + }, + { + h: 40, + s: 0.5, + b: 1, + name: 'light green' + }, + { + h: 40, + s: 1, + b: 0.5, + name: 'dark green' + }, + { + h: 40, + s: 1, + b: 1, + name: 'green' + }, + { + h: 45, + s: 0.5, + b: 1, + name: 'light turquoise' + }, + { + h: 45, + s: 1, + b: 0.5, + name: 'dark turquoise' + }, + { + h: 45, + s: 1, + b: 1, + name: 'turquoise' + }, + { + h: 50, + s: 0, + b: 1, + name: 'light sky blue' + }, + { + h: 50, + s: 0.5, + b: 0.5, + name: 'dark cyan' + }, + { + h: 50, + s: 0.5, + b: 1, + name: 'light cyan' + }, + { + h: 50, + s: 1, + b: 0.5, + name: 'dark cyan' + }, + { + h: 50, + s: 1, + b: 1, + name: 'cyan' + }, + { + h: 55, + s: 0, + b: 1, + name: 'light sky blue' + }, + { + h: 55, + s: 0.5, + b: 1, + name: 'light sky blue' + }, + { + h: 55, + s: 1, + b: 0.5, + name: 'dark blue' + }, + { + h: 55, + s: 1, + b: 1, + name: 'sky blue' + }, + { + h: 60, + s: 0, + b: 0.5, + name: 'gray' + }, + { + h: 60, + s: 0, + b: 1, + name: 'very light blue' + }, + { + h: 60, + s: 0.5, + b: 0.5, + name: 'blue' + }, + { + h: 60, + s: 0.5, + b: 1, + name: 'light blue' + }, + { + h: 60, + s: 1, + b: 0.5, + name: 'navy blue' + }, + { + h: 60, + s: 1, + b: 1, + name: 'blue' + }, + { + h: 65, + s: 0, + b: 1, + name: 'lavender' + }, + { + h: 65, + s: 0.5, + b: 0.5, + name: 'navy blue' + }, + { + h: 65, + s: 0.5, + b: 1, + name: 'light purple' + }, + { + h: 65, + s: 1, + b: 0.5, + name: 'dark navy blue' + }, + { + h: 65, + s: 1, + b: 1, + name: 'blue' + }, + { + h: 70, + s: 0, + b: 1, + name: 'lavender' + }, + { + h: 70, + s: 0.5, + b: 0.5, + name: 'navy blue' + }, + { + h: 70, + s: 0.5, + b: 1, + name: 'lavender blue' + }, + { + h: 70, + s: 1, + b: 0.5, + name: 'dark navy blue' + }, + { + h: 70, + s: 1, + b: 1, + name: 'blue' + }, + { + h: 75, + s: 0.5, + b: 1, + name: 'lavender' + }, + { + h: 75, + s: 1, + b: 0.5, + name: 'dark purple' + }, + { + h: 75, + s: 1, + b: 1, + name: 'purple' + }, + { + h: 80, + s: 0.5, + b: 1, + name: 'pinkish purple' + }, + { + h: 80, + s: 1, + b: 0.5, + name: 'dark purple' + }, + { + h: 80, + s: 1, + b: 1, + name: 'purple' + }, + { + h: 85, + s: 0, + b: 1, + name: 'light pink' + }, + { + h: 85, + s: 0.5, + b: 0.5, + name: 'purple' + }, + { + h: 85, + s: 0.5, + b: 1, + name: 'light fuchsia' + }, + { + h: 85, + s: 1, + b: 0.5, + name: 'dark fuchsia' + }, + { + h: 85, + s: 1, + b: 1, + name: 'fuchsia' + }, + { + h: 90, + s: 0.5, + b: 0.5, + name: 'dark fuchsia' + }, + { + h: 90, + s: 0.5, + b: 1, + name: 'hot pink' + }, + { + h: 90, + s: 1, + b: 0.5, + name: 'dark fuchsia' + }, + { + h: 90, + s: 1, + b: 1, + name: 'fuchsia' + }, + { + h: 95, + s: 0, + b: 1, + name: 'pink' + }, + { + h: 95, + s: 0.5, + b: 1, + name: 'light pink' + }, + { + h: 95, + s: 1, + b: 0.5, + name: 'dark magenta' + }, + { + h: 95, + s: 1, + b: 1, + name: 'magenta' + } +]; + +//returns text with color name +function _calculateColor(hsb) { + let colortext; + //round hue + if (hsb[0] !== 0) { + hsb[0] = Math.round(hsb[0] * 100); + let hue = hsb[0].toString().split(''); + const last = hue.length - 1; + hue[last] = parseInt(hue[last]); + //if last digit of hue is < 2.5 make it 0 + if (hue[last] < 2.5) { + hue[last] = 0; + //if last digit of hue is >= 2.5 and less than 7.5 make it 5 + } else if (hue[last] >= 2.5 && hue[last] < 7.5) { + hue[last] = 5; + } + //if hue only has two digits + if (hue.length === 2) { + hue[0] = parseInt(hue[0]); + //if last is greater than 7.5 + if (hue[last] >= 7.5) { + //add one to the tens + hue[last] = 0; + hue[0] = hue[0] + 1; + } + hsb[0] = hue[0] * 10 + hue[1]; + } else { + if (hue[last] >= 7.5) { + hsb[0] = 10; + } else { + hsb[0] = hue[last]; + } + } + } + //map brightness from 0 to 1 + hsb[2] = hsb[2] / 255; + //round saturation and brightness + for (let i = hsb.length - 1; i >= 1; i--) { + if (hsb[i] <= 0.25) { + hsb[i] = 0; + } else if (hsb[i] > 0.25 && hsb[i] < 0.75) { + hsb[i] = 0.5; + } else { + hsb[i] = 1; + } + } + //after rounding, if the values are hue 0, saturation 0 and brightness 1 + //look at color exceptions which includes several tones from white to gray + if (hsb[0] === 0 && hsb[1] === 0 && hsb[2] === 1) { + //round original hsb values + for (let i = 2; i >= 0; i--) { + originalHSB[i] = Math.round(originalHSB[i] * 10000) / 10000; + } + //compare with the values in the colorExceptions array + for (let e = 0; e < colorExceptions.length; e++) { + if ( + colorExceptions[e].h === originalHSB[0] && + colorExceptions[e].s === originalHSB[1] && + colorExceptions[e].b === originalHSB[2] + ) { + colortext = colorExceptions[e].name; + break; + } else { + //if there is no match return white + colortext = 'white'; + } + } + } else { + //otherwise, compare with values in colorLookUp + for (let i = 0; i < colorLookUp.length; i++) { + if ( + colorLookUp[i].h === hsb[0] && + colorLookUp[i].s === hsb[1] && + colorLookUp[i].b === hsb[2] + ) { + colortext = colorLookUp[i].name; + break; + } + } + } + return colortext; +} + +//gets rgba and returs a color name +p5.prototype._rgbColorName = function(arg) { + //conversts rgba to hsb + let hsb = color_conversion._rgbaToHSBA(arg); + //stores hsb in global variable + originalHSB = hsb; + //calculate color name + return _calculateColor([hsb[0], hsb[1], hsb[2]]); +}; + +export default p5; diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js index 4bcbf04d1d..5d6c7145c5 100644 --- a/src/accessibility/describe.js +++ b/src/accessibility/describe.js @@ -14,11 +14,9 @@ const labelContainer = '_Label'; //Label container const labelDescId = '_labelDesc'; //Label description const labelTableId = '_labelTable'; //Label Table const labelTableElId = '_lte_'; //Label Table Element -//dummy stores a copy of the DOM and previous descriptions -let dummy = { fallbackElements: {}, labelElements: {} }; /** - * Creates a screen-reader accessible description for the canvas. + * Creates a screen reader accessible description for the canvas. * The first parameter should be a string with a description of the canvas. * The second parameter is optional. If specified, it determines how the * description is displayed. @@ -80,41 +78,36 @@ p5.prototype.describe = function(text, display) { const cnvId = this.canvas.id; //calls function that adds punctuation for better screen reading text = _descriptionText(text); - - //if it is the first time describe() is called - if (!dummy[cnvId + 'fallbackDesc'] || !dummy[cnvId + 'labelDesc']) { - //store copy of body dom in dummy - _populateDummyDOM(cnvId); + //if there is no dummyDOM + if (!this.dummyDOM) { + this.dummyDOM = document.getElementById(cnvId).parentNode; } - - //check if text is different - if (dummy[cnvId + 'fallbackDesc'] !== text) { - //if html structure for description is ready - if (dummy[cnvId + 'updateFallbackDesc']) { + if (!this.descriptions) { + this.descriptions = {}; + } + //check if html structure for description is ready + if (this.descriptions.fallback) { + //check if text is different from current description + if (this.descriptions.fallback.innerHTML !== text) { //update description - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId + fallbackDescId - ).innerHTML = text; - //store updated description - dummy[cnvId + 'fallbackDesc'] = text; - } else { - //create fallback html structure - _describeFallbackHTML(cnvId, text); + this.descriptions.fallback.innerHTML = text; } + } else { + //create fallback html structure + this._describeHTML('fallback', text); } - //if display is LABEL and label text is different - if (display === this.LABEL && dummy[cnvId + 'labelDesc'] !== text) { - //if html structure for label is ready - if (dummy[cnvId + labelDescId]) { - //update label description - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId + labelDescId - ).innerHTML = text; - //store updated label description - dummy[cnvId + 'labelDesc'] = text; + //if display is LABEL + if (display === this.LABEL) { + //check if html structure for label is ready + if (this.descriptions.label) { + //check if text is different from current label + if (this.descriptions.label.innerHTML !== text) { + //update label description + this.descriptions.label.innerHTML = text; + } } else { //create label html structure - _describeLabelHTML(cnvId, text); + this._describeHTML('label', text); } } }; @@ -178,44 +171,41 @@ p5.prototype.describeElement = function(name, text, display) { name = name.replace(/[^a-zA-Z0-9 ]/g, ''); //store element description let inner = `${elementName}${text}`; - - //if it is the first time describeElement() is called - if ( - !dummy.fallbackElements[cnvId + name] || - !dummy.labelElements[cnvId + name] - ) { - //store copy of body dom in dummy - _populateDummyDOM(cnvId); + //if there is no dummyDOM + if (!this.dummyDOM) { + this.dummyDOM = document.getElementById(cnvId).parentNode; } - - //check if element description is different from current - if (dummy.fallbackElements[cnvId + name] !== inner) { - //if html structure for element description is ready - if (dummy.fallbackElements[cnvId + name]) { + if (!this.descriptions) { + this.descriptions = { fallbackElements: {} }; + } else if (!this.descriptions.fallbackElements) { + this.descriptions.fallbackElements = {}; + } + //check if html structure for element description is ready + if (this.descriptions.fallbackElements[name]) { + //if current element description is not the same as inner + if (this.descriptions.fallbackElements[name].innerHTML !== inner) { //update element description - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId + fallbackTableElId + name - ).innerHTML = inner; - //store updated element description - dummy.fallbackElements[cnvId + name] = inner; - } else { - //create fallback html structure - _descElementFallbackHTML(cnvId, name, inner); + this.descriptions.fallbackElements[name].innerHTML = inner; } + } else { + //create fallback html structure + this._describeElementHTML('fallback', name, inner); } - //if display is LABEL and label element description is different - if (display === this.LABEL && dummy.labelElements[cnvId + name] !== inner) { + //if display is LABEL + if (display === this.LABEL) { + if (!this.descriptions.labelElements) { + this.descriptions.labelElements = {}; + } //if html structure for label element description is ready - if (dummy.labelElements[cnvId + name]) { - //update label element description - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId + labelTableElId + name - ).innerHTML = inner; - //store updated label element description - dummy.labelElements[cnvId + name] = inner; + if (this.descriptions.labelElements[name]) { + //if label element description is different + if (this.descriptions.labelElements[name].innerHTML !== inner) { + //update label element description + this.descriptions.labelElements[name].innerHTML = inner; + } } else { //create label element html structure - _descElementLabelHTML(cnvId, name, inner); + this._describeElementHTML('label', name, inner); } } }; @@ -226,16 +216,6 @@ p5.prototype.describeElement = function(name, text, display) { * */ -//clear dummy -p5.prototype._clearDummy = function() { - dummy = { fallbackElements: {}, labelElements: {} }; -}; - -//stores html body in dummy -function _populateDummyDOM(cnvId) { - dummy[cnvId + 'DOM'] = document.getElementsByTagName('body')[0]; -} - // check that text is not LABEL or FALLBACK and ensure text ends with punctuation mark function _descriptionText(text) { if (text === 'label' || text === 'fallback') { @@ -244,6 +224,7 @@ function _descriptionText(text) { //if string does not end with '.' if ( !text.endsWith('.') && + !text.endsWith(';') && !text.endsWith(',') && !text.endsWith('?') && !text.endsWith('!') @@ -258,78 +239,72 @@ function _descriptionText(text) { * Helper functions for describe() */ -//creates fallback HTML structure -function _describeFallbackHTML(cnvId, text) { - //if there is no description container - if (!dummy[cnvId + descContainer]) { - //create description container +

for fallback description - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId - ).innerHTML = `

`; - //set container and fallbackDescId to true - dummy[cnvId + descContainer] = true; - dummy[cnvId + fallbackDescId] = true; - //if describeElement() has already created the container and added a table of elements - } else if (dummy[cnvId + fallbackTableId]) { - //create fallback description

before the table - dummy[cnvId + 'DOM'] - .querySelector('#' + cnvId + fallbackTableId) - .insertAdjacentHTML( - 'beforebegin', - `

` - ); - //set fallbackDescId to true - dummy[cnvId + fallbackDescId] = true; - } - //If the container for the description exists - if (dummy[cnvId + 'DOM'].querySelector('#' + cnvId + fallbackDescId)) { - //update description - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId + fallbackDescId - ).innerHTML = text; - //store updated description - dummy[cnvId + 'fallbackDesc'] = text; - //html structure is ready for any description updates - dummy[cnvId + 'updateFallbackDesc'] === true; - } - return; -} - -//If display is LABEL create a div adjacent to the canvas element with -//description text. -function _describeLabelHTML(cnvId, text) { - //if there is no label container - if (!dummy[cnvId + labelContainer]) { - //create label container +

for label description - dummy[cnvId + 'DOM'] - .querySelector('#' + cnvId) - .insertAdjacentHTML( - 'afterend', - `

` - ); - //set container and labelDescId to true - dummy[cnvId + labelContainer] = true; - dummy[cnvId + labelDescId] = true; - //if describeElement() has already created the container and added a table of elements - } else if (!dummy[cnvId + labelDescId] && dummy[cnvId + labelTableId]) { - //create label description

before the table - dummy[cnvId + 'DOM'] - .querySelector('#' + cnvId + labelTableId) - .insertAdjacentHTML('beforebegin', `

`); - //set fallbackDescId to true - dummy[cnvId + labelDescId] = true; +//creates HTML structure for canvas descriptions +p5.prototype._describeHTML = function(type, text) { + const cnvId = this.canvas.id; + if (type === 'fallback') { + //if there is no description container + if (!this.dummyDOM.querySelector(`#${cnvId + descContainer}`)) { + //if there are no accessible outputs (see textOutput() and gridOutput()) + let html = `

`; + if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutput`)) { + //create description container +

for fallback description + this.dummyDOM.querySelector(`#${cnvId}`).innerHTML = html; + } else { + //create description container +

for fallback description before outputs + this.dummyDOM + .querySelector(`#${cnvId}accessibleOutput`) + .insertAdjacentHTML('beforebegin', html); + } + } else { + //if describeElement() has already created the container and added a table of elements + //create fallback description

before the table + this.dummyDOM + .querySelector('#' + cnvId + fallbackTableId) + .insertAdjacentHTML( + 'beforebegin', + `

` + ); + } + //if the container for the description exists + this.descriptions.fallback = this.dummyDOM.querySelector( + `#${cnvId}${fallbackDescId}` + ); + this.descriptions.fallback.innerHTML = text; + return; + } else if (type === 'label') { + //if there is no label container + if (!this.dummyDOM.querySelector(`#${cnvId + labelContainer}`)) { + let html = `

`; + //if there are no accessible outputs (see textOutput() and gridOutput()) + if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutputLabel`)) { + //create label container +

for label description + this.dummyDOM + .querySelector('#' + cnvId) + .insertAdjacentHTML('afterend', html); + } else { + //create label container +

for label description before outputs + this.dummyDOM + .querySelector(`#${cnvId}accessibleOutputLabel`) + .insertAdjacentHTML('beforebegin', html); + } + } else if (this.dummyDOM.querySelector(`#${cnvId + labelTableId}`)) { + //if describeElement() has already created the container and added a table of elements + //create label description

before the table + this.dummyDOM + .querySelector(`#${cnvId + labelTableId}`) + .insertAdjacentHTML( + 'beforebegin', + `

` + ); + } + this.descriptions.label = this.dummyDOM.querySelector( + '#' + cnvId + labelDescId + ); + this.descriptions.label.innerHTML = text; + return; } - //update description - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId + labelDescId - ).innerHTML = text; - //store updated description - dummy[cnvId + 'labelDesc'] = text; - return; -} +}; /* * Helper functions for describeElement(). @@ -344,101 +319,96 @@ function _elementName(name) { if (name.endsWith('.') || name.endsWith(';') || name.endsWith(',')) { //replace last character with ':' name = name.replace(/.$/, ':'); - //if string n does not end with ':' } else if (!name.endsWith(':')) { + //if string n does not end with ':' //add ':'' at the end of string name = name + ':'; } return name; } -//creates fallback HTML structure for element descriptions -function _descElementFallbackHTML(cnvId, name, inner) { - //if there is no description container - if (!dummy[cnvId + descContainer]) { - //create container + table for element descriptions - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId - ).innerHTML = `
Canvas elements and their descriptions
`; - //set container and fallbackTableId to true - dummy[cnvId + descContainer] = true; - dummy[cnvId + fallbackTableId] = true; - //if describe() has already created the container and added a description - } else if (document.getElementById(cnvId + fallbackDescId)) { - //create fallback table for element description after fallback description - dummy[cnvId + 'DOM'] - .querySelector('#' + cnvId + fallbackDescId) - .insertAdjacentHTML( - 'afterend', - `
Canvas elements and their descriptions
` - ); - //set fallbackTableId to true - dummy[cnvId + fallbackTableId] = true; - } - //if it is the first time this element is being added to the table - if (!dummy.fallbackElements[cnvId + name] && dummy[cnvId + fallbackTableId]) { +//creates HTML structure for element descriptions +p5.prototype._describeElementHTML = function(type, name, text) { + const cnvId = this.canvas.id; + if (type === 'fallback') { + //if there is no description container + if (!this.dummyDOM.querySelector(`#${cnvId + descContainer}`)) { + //if there are no accessible outputs (see textOutput() and gridOutput()) + let html = `
Canvas elements and their descriptions
`; + if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutput`)) { + //create container + table for element descriptions + this.dummyDOM.querySelector('#' + cnvId).innerHTML = html; + } else { + //create container + table for element descriptions before outputs + this.dummyDOM + .querySelector(`#${cnvId}accessibleOutput`) + .insertAdjacentHTML('beforebegin', html); + } + } else if (!this.dummyDOM.querySelector('#' + cnvId + fallbackTableId)) { + //if describe() has already created the container and added a description + //and there is no table create fallback table for element description after + //fallback description + this.dummyDOM + .querySelector('#' + cnvId + fallbackDescId) + .insertAdjacentHTML( + 'afterend', + `
Canvas elements and their descriptions
` + ); + } //create a table row for the element let tableRow = document.createElement('tr'); tableRow.id = cnvId + fallbackTableElId + name; - dummy[cnvId + 'DOM'] + this.dummyDOM .querySelector('#' + cnvId + fallbackTableId) .appendChild(tableRow); //update element description - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId + fallbackTableElId + name - ).innerHTML = inner; - //store updated element description - dummy.fallbackElements[cnvId + name] = inner; - } -} -//If display is LABEL creates a div adjacent to the canvas element with -//a table, a row header cell with the name of the elements, -//and adds the description of the element in adjecent cell. -function _descElementLabelHTML(cnvId, name, inner) { - //if there is no label description container - if (!dummy[cnvId + labelContainer]) { - //create container + table for element descriptions - dummy[cnvId + 'DOM'] - .querySelector('#' + cnvId) - .insertAdjacentHTML( - 'afterend', - `
` - ); - //set container and labelTableId to true - dummy[cnvId + labelContainer] = true; - dummy[cnvId + labelTableId] = true; - //if describe() has already created the label container and added a description - } else if (dummy[cnvId + 'DOM'].querySelector('#' + cnvId + labelDescId)) { - //create label table for element description after label description - dummy[cnvId + 'DOM'] - .querySelector('#' + cnvId + labelDescId) - .insertAdjacentHTML( - 'afterend', - `
` - ); - //set labelTableId to true - dummy[cnvId + labelTableId] = true; - } - //if it is the first time this element is being added to the table - if (!dummy.labelElements[cnvId + name] && dummy[cnvId + labelTableId]) { + this.descriptions.fallbackElements[name] = this.dummyDOM.querySelector( + `#${cnvId}${fallbackTableElId}${name}` + ); + this.descriptions.fallbackElements[name].innerHTML = text; + return; + } else if (type === 'label') { + //If display is LABEL creates a div adjacent to the canvas element with + //a table, a row header cell with the name of the elements, + //and adds the description of the element in adjecent cell. + //if there is no label description container + if (!this.dummyDOM.querySelector(`#${cnvId + labelContainer}`)) { + //if there are no accessible outputs (see textOutput() and gridOutput()) + let html = `
`; + if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutputLabel`)) { + //create container + table for element descriptions + this.dummyDOM + .querySelector('#' + cnvId) + .insertAdjacentHTML('afterend', html); + } else { + //create container + table for element descriptions before outputs + this.dummyDOM + .querySelector(`#${cnvId}accessibleOutputLabel`) + .insertAdjacentHTML('beforebegin', html); + } + } else if (!this.dummyDOM.querySelector(`#${cnvId + labelTableId}`)) { + //if describe() has already created the label container and added a description + //and there is no table create label table for element description after + //label description + this.dummyDOM + .querySelector('#' + cnvId + labelDescId) + .insertAdjacentHTML( + 'afterend', + `
` + ); + } //create a table row for the element label description let tableRow = document.createElement('tr'); tableRow.id = cnvId + labelTableElId + name; - dummy[cnvId + 'DOM'] + this.dummyDOM .querySelector('#' + cnvId + labelTableId) .appendChild(tableRow); //update element label description - dummy[cnvId + 'DOM'].querySelector( - '#' + cnvId + labelTableElId + name - ).innerHTML = inner; - //store updated element label description - dummy.labelElements[cnvId + name] = inner; + this.descriptions.labelElements[name] = this.dummyDOM.querySelector( + `#${cnvId}${labelTableElId}${name}` + ); + this.descriptions.labelElements[name].innerHTML = text; } -} +}; export default p5; diff --git a/src/accessibility/gridOutput.js b/src/accessibility/gridOutput.js new file mode 100644 index 0000000000..8d8546faf1 --- /dev/null +++ b/src/accessibility/gridOutput.js @@ -0,0 +1,150 @@ +/** + * @module Environment + * @submodule Environment + * @for p5 + * @requires core + */ +import p5 from '../core/main'; + +//the functions in this file support updating the grid output + +//updates gridOutput +p5.prototype._updateGridOutput = function(idT) { + //if html structure is not there yet + if (!this.dummyDOM.querySelector(`#${idT}_summary`)) { + return; + } + let current = this._accessibleOutputs[idT]; + //create shape details list + let innerShapeDetails = _gridShapeDetails(idT, this.ingredients.shapes); + //create summary + let innerSummary = _gridSummary( + innerShapeDetails.numShapes, + this.ingredients.colors.background, + this.width, + this.height + ); + //create grid map + let innerMap = _gridMap(idT, this.ingredients.shapes); + //if it is different from current summary + if (innerSummary !== current.summary.innerHTML) { + //update + current.summary.innerHTML = innerSummary; + } + //if it is different from current map + if (innerMap !== current.map.innerHTML) { + //update + current.map.innerHTML = innerMap; + } + //if it is different from current shape details + if (innerShapeDetails.details !== current.shapeDetails.innerHTML) { + //update + current.shapeDetails.innerHTML = innerShapeDetails.details; + } + this._accessibleOutputs[idT] = current; +}; + +//creates spatial grid that maps the location of shapes +function _gridMap(idT, ingredients) { + let shapeNumber = 0; + let table = ''; + //create an array of arrays 10*10 of empty cells + let cells = Array.apply(null, Array(10)).map(function() {}); + for (let r in cells) { + cells[r] = Array.apply(null, Array(10)).map(function() {}); + } + for (let x in ingredients) { + for (let y in ingredients[x]) { + let fill; + if (x !== 'line') { + fill = `${ + ingredients[x][y].color + } ${x}`; + } else { + fill = `${ + ingredients[x][y].color + } ${x} midpoint`; + } + //if empty cell of location of shape is undefined + if (!cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX]) { + //fill it with shape info + cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX] = fill; + //if a shape is already in that location + } else { + //add it + cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX] = + cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX] + + ' ' + + fill; + } + shapeNumber++; + } + } + //make table based on array + for (let _r in cells) { + let row = ''; + for (let c in cells[_r]) { + row = row + ''; + if (cells[_r][c] !== undefined) { + row = row + cells[_r][c]; + } + row = row + ''; + } + table = table + row + ''; + } + return table; +} + +//creates grid summary +function _gridSummary(numShapes, background, width, height) { + let text = `${background} canvas, ${width} by ${height} pixels, contains ${ + numShapes[0] + }`; + if (numShapes[0] === 1) { + text = `${text} shape: ${numShapes[1]}`; + } else { + text = `${text} shapes: ${numShapes[1]}`; + } + return text; +} + +//creates list of shapes +function _gridShapeDetails(idT, ingredients) { + let shapeDetails = ''; + let shapes = ''; + let totalShapes = 0; + //goes trhough every shape type in ingredients + for (let x in ingredients) { + let shapeNum = 0; + for (let y in ingredients[x]) { + //it creates a line in a list + let line = `
  • ${ + ingredients[x][y].color + } ${x},`; + if (x === 'line') { + line = + line + + ` location = ${ingredients[x][y].pos}, length = ${ + ingredients[x][y].length + } pixels`; + } else { + line = line + ` location = ${ingredients[x][y].pos}`; + if (x !== 'point') { + line = line + `, area = ${ingredients[x][y].area} %`; + } + line = line + '
  • '; + } + shapeDetails = shapeDetails + line; + shapeNum++; + totalShapes++; + } + if (shapeNum > 1) { + shapes = `${shapes} ${shapeNum} ${x}s`; + } else { + shapes = `${shapes} ${shapeNum} ${x}`; + } + } + return { numShapes: [totalShapes, shapes], details: shapeDetails }; +} + +export default p5; diff --git a/src/accessibility/outputs.js b/src/accessibility/outputs.js new file mode 100644 index 0000000000..fdfa23e917 --- /dev/null +++ b/src/accessibility/outputs.js @@ -0,0 +1,533 @@ +/** + * @module Environment + * @submodule Environment + * @for p5 + * @requires core + */ + +import p5 from '../core/main'; + +/** + * textOutput() creates a screenreader + * accessible output that describes the shapes present on the canvas. + * The general description of the canvas includes canvas size, + * canvas color, and number of elements in the canvas + * (example: 'Your output is a, 400 by 400 pixels, lavender blue + * canvas containing the following 4 shapes:'). This description + * is followed by a list of shapes where the color, position, and area + * of each shape are described (example: "orange ellipse at top left + * covering 1% of the canvas"). Each element can be selected to get + * more details. A table of elements is also provided. In this table, + * shape, color, location, coordinates and area are described + * (example: "orange ellipse location=top left area=2"). + * + * textOutput() and texOutput(FALLBACK) + * make the output available in + * a sub DOM inside the canvas element which is accessible to screen readers. + * textOutput(LABEL) creates an + * additional div with the output adjacent to the canvas, this is useful + * for non-screen reader users that might want to display the output outside + * of the canvas' sub DOM as they code. However, using LABEL will create + * unnecessary redundancy for screen reader users. We recommend using LABEL + * only as part of the development process of a sketch and removing it before + * publishing or sharing with screen reader users. + * + * @method textOutput + * @param {Constant} [display] either FALLBACK or LABEL (Optional) + * + * @example + *
    + * + * textOutput(); + * background(148, 196, 0); + * fill(255, 0, 0); + * ellipse(20, 20, 20, 20); + * fill(0, 0, 255); + * rect(50, 50, 50, 50); + * + *
    + * + * + *
    + * + * let x = 0; + * function draw() { + * textOutput(); + * background(148, 196, 0); + * fill(255, 0, 0); + * ellipse(x, 20, 20, 20); + * fill(0, 0, 255); + * rect(50, 50, 50, 50); + * ellipse(20, 20, 20, 20); + * x += 0.1; + * } + * + *
    + * + */ + +p5.prototype.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'); + } + } +}; + +/** + * gridOutput() lays out the + * content of the canvas in the form of a grid (html table) based + * on the spatial location of each shape. A brief + * description of the canvas is available before the table output. + * This description includes: color of the background, size of the canvas, + * number of objects, and object types (example: "lavender blue canvas is + * 200 by 200 and contains 4 objects - 3 ellipses 1 rectangle"). The grid + * describes the content spatially, each element is placed on a cell of the + * table depending on its position. Within each cell an element the color + * and type of shape of that element are available (example: "orange ellipse"). + * These descriptions can be selected individually to get more details. + * A list of elements where shape, color, location, and area are described + * (example: "orange ellipse location=top left area=1%") is also available. + * + * gridOutput() and gridOutput(FALLBACK) + * make the output available in + * a sub DOM inside the canvas element which is accessible to screen readers. + * gridOutput(LABEL) creates an + * additional div with the output adjacent to the canvas, this is useful + * for non-screen reader users that might want to display the output outside + * of the canvas' sub DOM as they code. However, using LABEL will create + * unnecessary redundancy for screen reader users. We recommend using LABEL + * only as part of the development process of a sketch and removing it before + * publishing or sharing with screen reader users. + * + * @method gridOutput + * @param {Constant} [display] either FALLBACK or LABEL (Optional) + * + * @example + *
    + * + * gridOutput(); + * background(148, 196, 0); + * fill(255, 0, 0); + * ellipse(20, 20, 20, 20); + * fill(0, 0, 255); + * rect(50, 50, 50, 50); + * + *
    + * + * + *
    + * + * let x = 0; + * function draw() { + * gridOutput(); + * background(148, 196, 0); + * fill(255, 0, 0); + * ellipse(x, 20, 20, 20); + * fill(0, 0, 255); + * rect(50, 50, 50, 50); + * ellipse(20, 20, 20, 20); + * x += 0.1; + * } + * + *
    + * + */ + +p5.prototype.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 +p5.prototype._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 +p5.prototype._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: '' + }; + } + //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 = `
    `; + } else { + //create html structure after canvas description container + this.dummyDOM + .querySelector(`#${cnvId}_Description`) + .insertAdjacentHTML( + 'afterend', + `
    ` + ); + } + } + } 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', `
    `); + } else { + //create html structure after canvas label + this.dummyDOM + .querySelector(`#${cnvId}_Label`) + .insertAdjacentHTML('afterend', `
    `); + } + } + } + //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 = `
    Text Output

    `; + //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 = `
    Grid Output

    `; + //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 +p5.prototype._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) { + //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 +p5.prototype._accsBackground = function(args) { + //save current shapes as string in pShapes + this.ingredients.pShapes = JSON.stringify(this.ingredients.shapes); + //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 +p5.prototype._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 +p5.prototype._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(this.dist(args[0], args[1], args[2], args[3])); + //get position of end points + let p1 = _getPos([args[0], [1]], this.width, this.height); + let p2 = _getPos([args[2], [3]], this.width, this.height); + 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 = _getArea(f, args, this.width, this.height); + } + //get middle of shapes + //calculate position using middle of shape + include.pos = _getPos(middle, this.width, this.height); + //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 +function _getPos(args, canvasWidth, canvasHeight) { + if (args[0] < 0.4 * canvasWidth) { + if (args[1] < 0.4 * canvasHeight) { + return 'top left'; + } else if (args[1] > 0.6 * canvasHeight) { + return 'bottom left'; + } else { + return 'mid left'; + } + } else if (args[0] > 0.6 * canvasWidth) { + if (args[1] < 0.4 * canvasHeight) { + return 'top right'; + } else if (args[1] > 0.6 * canvasHeight) { + return 'bottom right'; + } else { + return 'mid right'; + } + } else { + if (args[1] < 0.4 * canvasHeight) { + return 'top middle'; + } else if (args[1] > 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 +function _getArea(objectType, shapeArgs, canvasWidth, canvasHeight) { + 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 + } + + return Math.round(objectArea * 100 / (canvasWidth * canvasHeight)); +} + +export default p5; diff --git a/src/accessibility/textOutput.js b/src/accessibility/textOutput.js new file mode 100644 index 0000000000..ac2d38ec5a --- /dev/null +++ b/src/accessibility/textOutput.js @@ -0,0 +1,121 @@ +/** + * @module Environment + * @submodule Environment + * @for p5 + * @requires core + */ +import p5 from '../core/main'; + +//the functions in this file support updating the text output + +//updates textOutput +p5.prototype._updateTextOutput = function(idT) { + //if html structure is not there yet + if (!this.dummyDOM.querySelector(`#${idT}_summary`)) { + return; + } + let current = this._accessibleOutputs[idT]; + //create shape list + let innerList = _shapeList(idT, this.ingredients.shapes); + //create output summary + let innerSummary = _textSummary( + innerList.numShapes, + this.ingredients.colors.background, + this.width, + this.height + ); + //create shape details + let innerShapeDetails = _shapeDetails(idT, this.ingredients.shapes); + //if it is different from current summary + if (innerSummary !== current.summary.innerHTML) { + //update + current.summary.innerHTML = innerSummary; + } + //if it is different from current shape list + if (innerList.listShapes !== current.list.innerHTML) { + //update + current.list.innerHTML = innerList.listShapes; + } + //if it is different from current shape details + if (innerShapeDetails !== current.shapeDetails.innerHTML) { + //update + current.shapeDetails.innerHTML = innerShapeDetails; + } + this._accessibleOutputs[idT] = current; +}; + +//Builds textOutput summary +function _textSummary(numShapes, background, width, height) { + let text = `Your output is a, ${width} by ${height} pixels, ${background} canvas containing the following`; + if (numShapes === 1) { + text = `${text} shape:`; + } else { + text = `${text} ${numShapes} shapes:`; + } + return text; +} + +//Builds textOutput table with shape details +function _shapeDetails(idT, ingredients) { + let shapeDetails = ''; + let shapeNumber = 0; + //goes trhough every shape type in ingredients + for (let x in ingredients) { + //and for every shape + for (let y in ingredients[x]) { + //it creates a table row + let row = `${ + ingredients[x][y].color + } ${x}`; + if (x === 'line') { + row = + row + + `location = ${ingredients[x][y].pos}length = ${ + ingredients[x][y].length + } pixels`; + } else { + row = row + `location = ${ingredients[x][y].pos}`; + if (x !== 'point') { + row = row + ` area = ${ingredients[x][y].area}%`; + } + row = row + ''; + } + shapeDetails = shapeDetails + row; + shapeNumber++; + } + } + return shapeDetails; +} + +//Builds textOutput shape list +function _shapeList(idT, ingredients) { + let shapeList = ''; + let shapeNumber = 0; + //goes trhough every shape type in ingredients + for (let x in ingredients) { + for (let y in ingredients[x]) { + //it creates a line in a list + let _line = `
  • ${ + ingredients[x][y].color + } ${x}`; + if (x === 'line') { + _line = + _line + + `, ${ingredients[x][y].pos}, ${ + ingredients[x][y].length + } pixels long.
  • `; + } else { + _line = _line + `, at ${ingredients[x][y].pos}`; + if (x !== 'point') { + _line = _line + `, covering ${ingredients[x][y].area}% of the canvas`; + } + _line = _line + `.`; + } + shapeList = shapeList + _line; + shapeNumber++; + } + } + return { numShapes: shapeNumber, listShapes: shapeList }; +} + +export default p5; diff --git a/src/app.js b/src/app.js index a5022b200e..54394f6645 100644 --- a/src/app.js +++ b/src/app.js @@ -21,7 +21,11 @@ import './core/shape/2d_primitives'; import './core/shape/attributes'; import './core/shape/curves'; import './core/shape/vertex'; - +//accessibility +import './accessibility/outputs'; +import './accessibility/textOutput'; +import './accessibility/gridOutput'; +import './accessibility/color_namer'; // color import './color/color_conversion'; import './color/creating_reading'; diff --git a/src/core/main.js b/src/core/main.js index 4c2f4428e9..dbbb79c0ed 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -166,6 +166,12 @@ class p5 { // PRIVATE p5 PROPERTIES AND METHODS ////////////////////////////////////////////// + this._accessibleOutputs = { + text: false, + grid: false, + textLabel: false, + gridLabel: false + }; this._setupDone = false; // for handling hidpi this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; @@ -355,6 +361,9 @@ class p5 { this._lastFrameTime = window.performance.now(); this._setupDone = true; + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._updateAccsOutput(); + } }; this._draw = () => { diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 6d7e93ae99..31f144a2fd 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -52,6 +52,12 @@ p5.Renderer2D.prototype.background = function(...args) { const curFill = this._getFill(); // create background rect const color = this._pInst.color(...args); + + //accessible Outputs + if (this._pInst._addAccsOutput()) { + this._pInst._accsBackground(color.levels); + } + const newFill = color.toString(); this._setFill(newFill); @@ -80,11 +86,21 @@ p5.Renderer2D.prototype.clear = function() { p5.Renderer2D.prototype.fill = function(...args) { const color = this._pInst.color(...args); this._setFill(color.toString()); + + //accessible Outputs + if (this._pInst._addAccsOutput()) { + this._pInst._accsCanvasColors('fill', color.levels); + } }; p5.Renderer2D.prototype.stroke = function(...args) { const color = this._pInst.color(...args); this._setStroke(color.toString()); + + //accessible Outputs + if (this._pInst._addAccsOutput()) { + this._pInst._accsCanvasColors('stroke', color.levels); + } }; p5.Renderer2D.prototype.erase = function(opacityFill, opacityStroke) { diff --git a/src/core/rendering.js b/src/core/rendering.js index 55279b1e86..2c2793431e 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -169,6 +169,10 @@ p5.prototype.resizeCanvas = function(w, h, noRedraw) { this.redraw(); } } + //accessible Outputs + if (this._addAccsOutput()) { + this._updateAccsOutput(); + } }; /** diff --git a/src/core/shape/2d_primitives.js b/src/core/shape/2d_primitives.js index 49e856ea1f..fca2e4f76e 100644 --- a/src/core/shape/2d_primitives.js +++ b/src/core/shape/2d_primitives.js @@ -210,6 +210,19 @@ p5.prototype.arc = function(x, y, w, h, start, stop, mode, detail) { mode, detail ); + + //accessible Outputs + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._accsOutput('arc', [ + vals.x, + vals.y, + vals.w, + vals.h, + angles.start, + angles.stop, + mode + ]); + } } return this; @@ -311,6 +324,11 @@ p5.prototype._renderEllipse = function(x, y, w, h, detailX) { const vals = canvas.modeAdjust(x, y, w, h, this._renderer._ellipseMode); this._renderer.ellipse([vals.x, vals.y, vals.w, vals.h, detailX]); + //accessible Outputs + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._accsOutput('ellipse', [vals.x, vals.y, vals.w, vals.h]); + } + return this; }; @@ -367,6 +385,11 @@ p5.prototype.line = function(...args) { this._renderer.line(...args); } + //accessible Outputs + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._accsOutput('line', args); + } + return this; }; @@ -438,6 +461,10 @@ p5.prototype.point = function(...args) { ); } else { this._renderer.point(...args); + //accessible Outputs + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._accsOutput('point', args); + } } } @@ -503,6 +530,10 @@ p5.prototype.quad = function(...args) { args[6], args[7], 0); } else { this._renderer.quad(...args); + //accessibile outputs + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._accsOutput('quadrilateral', args); + } } } @@ -654,6 +685,11 @@ p5.prototype._renderRect = function() { args[i] = arguments[i]; } this._renderer.rect(args); + + //accessible outputs + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._accsOutput('rectangle', [vals.x, vals.y, vals.w, vals.h]); + } } return this; @@ -690,6 +726,11 @@ p5.prototype.triangle = function(...args) { this._renderer.triangle(args); } + //accessible outputs + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._accsOutput('triangle', args); + } + return this; }; diff --git a/src/core/structure.js b/src/core/structure.js index 2bc62aaebb..fa1d672998 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -477,6 +477,9 @@ p5.prototype.redraw = function(n) { }; for (let idxRedraw = 0; idxRedraw < numberOfRedraws; idxRedraw++) { context.resetMatrix(); + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._updateAccsOutput(); + } if (context._renderer.isP3D) { context._renderer._update(); } diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js index 96ec6d0d8b..41292277f5 100644 --- a/test/unit/accessibility/describe.js +++ b/test/unit/accessibility/describe.js @@ -1,14 +1,8 @@ suite('describe', function() { let myp5; - let myp5Container; let myID = 'myCanvasID'; - let a = 'a'; - let b = 'b'; - let c = 'c'; setup(function(done) { - myp5Container = document.createElement('div'); - document.body.appendChild(myp5Container); new p5(function(p) { p.setup = function() { let cnv = p.createCanvas(100, 100); @@ -16,64 +10,18 @@ suite('describe', function() { myp5 = p; done(); }; - }, myp5Container); + }); }); teardown(function() { - myp5._clearDummy(); myp5.remove(); - if (myp5Container && myp5Container.parentNode) { - myp5Container.parentNode.removeChild(myp5Container); - } - p5Container = null; }); suite('p5.prototype.describe', function() { - let expected = 'a.'; test('should be a function', function() { assert.ok(myp5.describe); assert.typeOf(myp5.describe, 'function'); }); - test('should create description as fallback', function() { - myp5.describe(a); - let actual = document.getElementById(myID + '_fallbackDesc').innerHTML; - assert.deepEqual(actual, expected); - }); - test('should not add extra period if string ends in "."', function() { - myp5.describe('a.'); - let actual = document.getElementById(myID + '_fallbackDesc').innerHTML; - assert.deepEqual(actual, expected); - }); - test.skip('should not add period if string ends in "!" or "?', function() { - myp5.describe('A!'); - let actual = document.getElementById(myID + '_fallbackDesc').innerHTML; - if (actual === 'A!') { - myp5.describe('A?'); - - actual = document.getElementById(myID + '_fallbackDesc').innerHTML; - assert.deepEqual(actual, 'A?'); - } - }); - test('should create description when called after describeElement()', function() { - myp5.describeElement(b, c); - myp5.describe(a); - - let actual = document.getElementById(myID + '_fallbackDesc').innerHTML; - assert.deepEqual(actual, expected); - }); - test('should create Label adjacent to canvas', function() { - myp5.describe(a, myp5.LABEL); - - let actual = document.getElementById(myID + '_labelDesc').innerHTML; - assert.deepEqual(actual, expected); - }); - test('should create Label adjacent to canvas when label of element already exists', function() { - myp5.describeElement(b, c, myp5.LABEL); - myp5.describe(a, myp5.LABEL); - - let actual = document.getElementById(myID + '_labelDesc').innerHTML; - assert.deepEqual(actual, expected); - }); test('wrong param type at #0', function() { assert.validationError(function() { myp5.describe(1, myp5.LABEL); @@ -93,55 +41,50 @@ suite('describe', function() { 'description should not be LABEL or FALLBACK' ); }); - }); - - suite('p5.prototype.describeElement', function() { - let expected = 'a:b.'; - test('should be a function', function() { - assert.ok(myp5.describeElement); - assert.typeOf(myp5.describeElement, 'function'); - }); - test('should create element description as fallback', function() { - myp5.describeElement(a, b); - let actual = document.getElementById(myID + '_fte_' + a).innerHTML; - assert.deepEqual(actual, expected); + test('should create description as fallback', function() { + myp5.describe('a'); + let actual = document.getElementById(myID + '_fallbackDesc'); + assert.deepEqual(actual.innerHTML, 'a.'); }); - test('should not add extra ":" if element name ends in colon', function() { - myp5.describeElement('a:', 'b.'); - let actual = document.getElementById(myID + '_fte_a').innerHTML; - assert.deepEqual(actual, expected); + test('should not add extra period if string ends in "."', function() { + myp5.describe('A.'); + let actual = document.getElementById(myID + '_fallbackDesc'); + assert.deepEqual(actual.innerHTML, 'A.'); }); - test('should replace ";", ",", "." for ":" in element name', function() { - let actual; - myp5.describeElement('a;', 'b.'); - if (document.getElementById(myID + '_fte_a').innerHTML === expected) { - myp5.describeElement('a,', 'b.'); - if (document.getElementById(myID + '_fte_a').innerHTML === expected) { - myp5.describeElement('a.', 'b.'); - actual = document.getElementById(myID + '_fte_a').innerHTML; - assert.deepEqual(actual, expected); - } + test('should not add period if string ends in "!" or "?', function() { + myp5.describe('A!'); + let actual = document.getElementById(myID + '_fallbackDesc'); + if (actual.innerHTML === 'A!') { + myp5.describe('A?'); + + actual = document.getElementById(myID + '_fallbackDesc'); + assert.deepEqual(actual.innerHTML, 'A?'); } }); - test('should create element description when called after describe()', function() { - myp5.describe(c); - myp5.describeElement(a, b); - - let actual = document.getElementById(myID + '_fte_' + a).innerHTML; - assert.deepEqual(actual, expected); + test('should create description when called after describeElement()', function() { + myp5.describeElement('b', 'c'); + myp5.describe('a'); + let actual = document.getElementById(myID + '_fallbackDesc'); + assert.deepEqual(actual.innerHTML, 'a.'); }); - test('should create element label adjacent to canvas', function() { - myp5.describeElement(a, b, myp5.LABEL); + test('should create Label adjacent to canvas', function() { + myp5.describe('a', myp5.LABEL); - const actual = document.getElementById(myID + '_lte_' + a).innerHTML; - assert.deepEqual(actual, expected); + let actual = document.getElementById(myID + '_labelDesc'); + assert.deepEqual(actual.innerHTML, 'a.'); }); - test('should create element label adjacent to canvas when called after describe()', function() { - myp5.describe(c, myp5.LABEL); - myp5.describeElement(a, b, myp5.LABEL); + test('should create Label adjacent to canvas when label of element already exists', function() { + myp5.describeElement('ba', 'c', myp5.LABEL); + myp5.describe('a', myp5.LABEL); + let actual = document.getElementById(myID + '_labelDesc'); + assert.deepEqual(actual.innerHTML, 'a.'); + }); + }); - const actual = document.getElementById(myID + '_lte_' + a).innerHTML; - assert.deepEqual(actual, expected); + suite('p5.prototype.describeElement', function() { + test('should be a function', function() { + assert.ok(myp5.describeElement); + assert.typeOf(myp5.describeElement, 'function'); }); test('wrong param type at #0 and #1', function() { assert.validationError(function() { @@ -156,7 +99,7 @@ suite('describe', function() { test('err when LABEL at param #0', function() { assert.throws( function() { - myp5.describeElement(myp5.LABEL, b); + myp5.describeElement(myp5.LABEL, 'b'); }, Error, 'element name should not be LABEL or FALLBACK' @@ -165,11 +108,56 @@ suite('describe', function() { test('err when LABEL at param #1', function() { assert.throws( function() { - myp5.describeElement(a, myp5.LABEL); + myp5.describeElement('a', myp5.LABEL); }, Error, 'description should not be LABEL or FALLBACK' ); }); + test('should create element description as fallback', function() { + myp5.describeElement('az', 'b'); + let actual = document.getElementById(myID + '_fte_az').innerHTML; + assert.deepEqual(actual, 'az:b.'); + }); + test('should not add extra ":" if element name ends in colon', function() { + myp5.describeElement('ab:', 'b.'); + let actual = document.getElementById(myID + '_fte_ab').innerHTML; + assert.deepEqual(actual, 'ab:b.'); + }); + test('should replace ";", ",", "." for ":" in element name', function() { + let actual; + myp5.describeElement('ac;', 'b.'); + if ( + document.getElementById(myID + '_fte_ac').innerHTML === + 'ac:b.' + ) { + myp5.describeElement('ad,', 'b.'); + if ( + document.getElementById(myID + '_fte_ad').innerHTML === + 'ad:b.' + ) { + myp5.describeElement('ae.', 'b.'); + actual = document.getElementById(myID + '_fte_ae').innerHTML; + assert.deepEqual(actual, 'ae:b.'); + } + } + }); + test('should create element description when called after describe()', function() { + myp5.describe('c'); + myp5.describeElement('af', 'b'); + let actual = document.getElementById(myID + '_fte_af').innerHTML; + assert.deepEqual(actual, 'af:b.'); + }); + test('should create element label adjacent to canvas', function() { + myp5.describeElement('ag', 'b', myp5.LABEL); + const actual = document.getElementById(myID + '_lte_ag').innerHTML; + assert.deepEqual(actual, 'ag:b.'); + }); + test('should create element label adjacent to canvas when called after describe()', function() { + myp5.describe('c', myp5.LABEL); + myp5.describeElement('ah:', 'b', myp5.LABEL); + const actual = document.getElementById(myID + '_lte_ah').innerHTML; + assert.deepEqual(actual, 'ah:b.'); + }); }); }); diff --git a/test/unit/accessibility/outputs.js b/test/unit/accessibility/outputs.js new file mode 100644 index 0000000000..6998d8e718 --- /dev/null +++ b/test/unit/accessibility/outputs.js @@ -0,0 +1,314 @@ +suite('outputs', function() { + let myp5; + let myID = 'myCanvasID'; + + setup(function(done) { + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id(myID); + myp5 = p; + done(); + }; + }); + }); + + teardown(function() { + myp5.remove(); + }); + + suite('p5.prototype.textOutput', function() { + test('should be a function', function() { + assert.ok(myp5.textOutput); + assert.typeOf(myp5.textOutput, 'function'); + }); + test('wrong param type at #0', function() { + assert.validationError(function() { + myp5.textOutput(1); + }); + }); + let expected = + 'Your output is a, 100 by 100 pixels, white canvas containing the following shape:'; + test('should create output as fallback', function() { + return new Promise(function(resolve, reject) { + let actual = ''; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + p.textOutput(); + p.line(0, 0, 100, 100); + }; + p.draw = function() { + if (p.frameCount === 1) { + actual = document.getElementById('myCanvasIDtextOutput_summary') + .innerHTML; + if (actual === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + actual); + } + p.remove(); + } + }; + }); + }); + }); + test('should create output as label', function() { + return new Promise(function(resolve, reject) { + let label = ''; + let fallback = ''; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + }; + p.draw = function() { + p.textOutput(p.LABEL); + p.line(0, 0, 100, 100); + if (p.frameCount === 2) { + label = document.getElementById( + 'myCanvasIDtextOutputLabel_summary' + ).innerHTML; + fallback = document.getElementById('myCanvasIDtextOutput_summary') + .innerHTML; + if (label === expected && fallback === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + label); + } + p.remove(); + } + }; + }); + }); + }); + test('should create text output for arc()', function() { + return new Promise(function(resolve, reject) { + expected = + '
  • red arc, at middle, covering 31% of the canvas.
  • '; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + }; + p.draw = function() { + p.textOutput(); + p.fill(255, 0, 0); + p.arc(50, 50, 80, 80, 0, p.PI + p.QUARTER_PI); + if (p.frameCount === 2) { + actual = document.getElementById('myCanvasIDtextOutput_list') + .innerHTML; + if (actual === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + actual); + } + p.remove(); + } + }; + }); + }); + }); + test('should create text output for ellipse()', function() { + return new Promise(function(resolve, reject) { + expected = + '
  • green circle, at middle, covering 24% of the canvas.
  • '; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + p.textOutput(); + p.fill(0, 255, 0); + p.ellipse(56, 46, 55, 55); + }; + p.draw = function() { + if (p.frameCount === 1) { + actual = document.getElementById('myCanvasIDtextOutput_list') + .innerHTML; + if (actual === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + actual); + } + p.remove(); + } + }; + }); + }); + }); + test('should create text output for triangle()', function() { + return new Promise(function(resolve, reject) { + expected = + '
  • green triangle, at top left, covering 13% of the canvas.
  • '; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + p.textOutput(); + p.fill(0, 255, 0); + p.triangle(0, 0, 0, 50, 50, 0); + }; + p.draw = function() { + if (p.frameCount === 1) { + actual = document.getElementById('myCanvasIDtextOutput_list') + .innerHTML; + if (actual === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + actual); + } + p.remove(); + } + }; + }); + }); + }); + }); + + suite('p5.prototype.gridOutput', function() { + test('should be a function', function() { + assert.ok(myp5.gridOutput); + assert.typeOf(myp5.gridOutput, 'function'); + }); + test('wrong param type at #0', function() { + assert.validationError(function() { + myp5.gridOutput(1); + }); + }); + let expected = + 'white canvas, 100 by 100 pixels, contains 1 shape: 1 square'; + test('should create output as fallback', function() { + return new Promise(function(resolve, reject) { + let actual = ''; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + p.gridOutput(); + p.rect(0, 0, 100, 100); + }; + p.draw = function() { + if (p.frameCount === 1) { + actual = document.getElementById('myCanvasIDgridOutput_summary') + .innerHTML; + if (actual === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + actual); + } + p.remove(); + } + }; + }); + }); + }); + test('should create output as label', function() { + return new Promise(function(resolve, reject) { + let label = ''; + let fallback = ''; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + }; + p.draw = function() { + p.gridOutput(p.LABEL); + p.square(0, 0, 100, 100); + if (p.frameCount === 2) { + label = document.getElementById( + 'myCanvasIDgridOutputLabel_summary' + ).innerHTML; + fallback = document.getElementById('myCanvasIDgridOutput_summary') + .innerHTML; + if (label === expected && fallback === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + label); + } + p.remove(); + } + }; + }); + }); + }); + test('should create text output for quad()', function() { + return new Promise(function(resolve, reject) { + expected = 'red quadrilateral, location = top left, area = 45 %'; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + }; + p.draw = function() { + p.gridOutput(); + p.fill(255, 0, 0); + p.quad(0, 0, 80, 0, 50, 50, 0, 100); + if (p.frameCount === 2) { + actual = document.getElementById('myCanvasIDgridOutputshape0') + .innerHTML; + if (actual === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + actual); + } + p.remove(); + } + }; + }); + }); + }); + test('should create text output for point()', function() { + return new Promise(function(resolve, reject) { + expected = 'dark fuchsia point, location = bottom right'; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + p.gridOutput(); + p.stroke('purple'); + p.point(85, 75); + }; + p.draw = function() { + if (p.frameCount === 1) { + actual = document.getElementById('myCanvasIDgridOutputshape0') + .innerHTML; + if (actual === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + actual); + } + p.remove(); + } + }; + }); + }); + }); + test('should create text output for triangle()', function() { + return new Promise(function(resolve, reject) { + expected = 'green triangle, location = top left, area = 13 %'; + new p5(function(p) { + p.setup = function() { + let cnv = p.createCanvas(100, 100); + cnv.id('myCanvasID'); + p.gridOutput(); + p.fill(0, 255, 0); + p.triangle(0, 0, 0, 50, 50, 0); + }; + p.draw = function() { + if (p.frameCount === 1) { + actual = document.getElementById('myCanvasIDgridOutputshape0') + .innerHTML; + if (actual === expected) { + resolve(); + } else { + reject(' expected: ' + expected + ' ---> found: ' + actual); + } + p.remove(); + } + }; + }); + }); + }); + }); +}); diff --git a/test/unit/spec.js b/test/unit/spec.js index c632632d75..acc426b5fd 100644 --- a/test/unit/spec.js +++ b/test/unit/spec.js @@ -1,5 +1,5 @@ var spec = { - accessibility: ['describe'], + accessibility: ['describe', 'outputs'], color: ['color_conversion', 'creating_reading', 'p5.Color', 'setting'], core: [ '2d_primitives',