diff --git a/.all-contributorsrc b/.all-contributorsrc index 514d88aace..9ce7dbd9a1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3059,6 +3059,150 @@ "contributions": [ "doc" ] + }, + { + "login": "tapioca24", + "name": "tapioca24", + "avatar_url": "https://avatars.githubusercontent.com/u/12683107?v=4", + "profile": "https://github.com/tapioca24", + "contributions": [ + "plugin" + ] + }, + { + "login": "Qianqianye", + "name": "Qianqian Ye", + "avatar_url": "https://avatars.githubusercontent.com/u/18587130?v=4", + "profile": "http://qianqian-ye.com", + "contributions": [ + "code", + "design", + "doc", + "eventOrganizing", + "review", + "translation" + ] + }, + { + "login": "adarrssh", + "name": "Adarsh", + "avatar_url": "https://avatars.githubusercontent.com/u/85433137?v=4", + "profile": "https://github.com/adarrssh", + "contributions": [ + "translation" + ] + }, + { + "login": "kaabe1", + "name": "kaabe1", + "avatar_url": "https://avatars.githubusercontent.com/u/78185255?v=4", + "profile": "https://github.com/kaabe1", + "contributions": [ + "design", + "eventOrganizing" + ] + }, + { + "login": "Guirdo", + "name": "Seb MΓ©ndez", + "avatar_url": "https://avatars.githubusercontent.com/u/21044700?v=4", + "profile": "https://www.guirdo.xyz/", + "contributions": [ + "translation" + ] + }, + { + "login": "3ru", + "name": "Ryuya", + "avatar_url": "https://avatars.githubusercontent.com/u/69892552?v=4", + "profile": "https://github.com/3ru", + "contributions": [ + "bug", + "review", + "code" + ] + }, + { + "login": "LEMIBANDDEXARI", + "name": "LEMIBANDDEXARI", + "avatar_url": "https://avatars.githubusercontent.com/u/70129787?v=4", + "profile": "https://github.com/LEMIBANDDEXARI", + "contributions": [ + "translation" + ] + }, + { + "login": "probablyvivek", + "name": "Vivek Tiwari", + "avatar_url": "https://avatars.githubusercontent.com/u/25459353?v=4", + "profile": "https://linktr.ee/probablyvivek", + "contributions": [ + "translation" + ] + }, + { + "login": "KevinGrajeda", + "name": "Kevin Grajeda", + "avatar_url": "https://avatars.githubusercontent.com/u/60023139?v=4", + "profile": "https://github.com/KevinGrajeda", + "contributions": [ + "code" + ] + }, + { + "login": "anniezhengg", + "name": "anniezhengg", + "avatar_url": "https://avatars.githubusercontent.com/u/78184655?v=4", + "profile": "https://github.com/anniezhengg", + "contributions": [ + "code", + "design" + ] + }, + { + "login": "SNP0301", + "name": "Seung-Gi Kim(David)", + "avatar_url": "https://avatars.githubusercontent.com/u/68281918?v=4", + "profile": "https://github.com/SNP0301", + "contributions": [ + "translation" + ] + }, + { + "login": "IkeB108", + "name": "Ike Bischof", + "avatar_url": "https://avatars.githubusercontent.com/u/56776763?v=4", + "profile": "https://ikebot108.weebly.com/", + "contributions": [ + "code" + ] + }, + { + "login": "ongzzzzzz", + "name": "Ong Zhi Zheng", + "avatar_url": "https://avatars.githubusercontent.com/u/47311100?v=4", + "profile": "https://ongzz.ml", + "contributions": [ + "plugin" + ] + }, + { + "login": "bsubbaraman", + "name": "bsubbaraman", + "avatar_url": "https://avatars.githubusercontent.com/u/11969085?v=4", + "profile": "https://github.com/bsubbaraman", + "contributions": [ + "plugin" + ] + }, + { + "login": "jdeboi", + "name": "Jenna deBoisblanc", + "avatar_url": "https://avatars.githubusercontent.com/u/1548679?v=4", + "profile": "http://jdeboi.com", + "contributions": [ + "plugin" + ] } ], "repoType": "github", diff --git a/.github/ISSUE_TEMPLATE/discussion.yml b/.github/ISSUE_TEMPLATE/discussion.yml index 5946f12e9d..115fb834cb 100644 --- a/.github/ISSUE_TEMPLATE/discussion.yml +++ b/.github/ISSUE_TEMPLATE/discussion.yml @@ -1,6 +1,6 @@ name: πŸ’­ Discussion description: This template is for starting a discussion. -labels: [discussion] +labels: [Discussion] body: - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml index 60d4e05ada..9d4808d754 100644 --- a/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml +++ b/.github/ISSUE_TEMPLATE/existing-feature-enhancement.yml @@ -1,38 +1,38 @@ name: πŸ’‘ Existing Feature Enhancement description: This template is for suggesting an improvement for an existing feature. -labels: [enhancement] +labels: [Enhancement] body: -- type: textarea - attributes: - label: Increasing Access - description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to p5.js? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) - validations: - required: true -- type: checkboxes - id: sub-area - attributes: - label: Most appropriate sub-area of p5.js? - description: You may select more than one. - options: - - label: Accessibility (Web Accessibility) - - label: Build tools and processes - - label: Color - - label: Core/Environment/Rendering - - label: Data - - label: DOM - - label: Events - - label: Friendly error system - - label: Image - - label: IO (Input/Output) - - label: Localization - - label: Math - - label: Unit Testing - - label: Typography - - label: Utilities - - label: WebGL - - label: Other (specify if possible) -- type: textarea - attributes: - label: Feature enhancement details - validations: - required: true + - type: textarea + attributes: + label: Increasing Access + description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to p5.js? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) + validations: + required: true + - type: checkboxes + id: sub-area + attributes: + label: Most appropriate sub-area of p5.js? + description: You may select more than one. + options: + - label: Accessibility + - label: Color + - label: Core/Environment/Rendering + - label: Data + - label: DOM + - label: Events + - label: Image + - label: IO + - label: Math + - label: Typography + - label: Utilities + - label: WebGL + - label: Build Process + - label: Unit Testing + - label: Internalization + - label: Friendly Errors + - label: Other (specify if possible) + - type: textarea + attributes: + label: Feature enhancement details + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 88f7531899..dd300bf37c 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,38 +1,38 @@ name: 🌱 New Feature Request description: This template is for requesting a new feature be added. -labels: [feature request] +labels: [Feature Request] body: -- type: textarea - attributes: - label: Increasing Access - description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to p5.js? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) - validations: - required: true -- type: checkboxes - id: sub-area - attributes: - label: Most appropriate sub-area of p5.js? - description: You may select more than one. - options: - - label: Accessibility (Web Accessibility) - - label: Build tools and processes - - label: Color - - label: Core/Environment/Rendering - - label: Data - - label: DOM - - label: Events - - label: Friendly error system - - label: Image - - label: IO (Input/Output) - - label: Localization - - label: Math - - label: Unit Testing - - label: Typography - - label: Utilities - - label: WebGL - - label: Other (specify if possible) -- type: textarea - attributes: - label: Feature request details - validations: - required: true \ No newline at end of file + - type: textarea + attributes: + label: Increasing Access + description: How would this new feature help [increase access](https://github.com/processing/p5.js/blob/main/contributor_docs/access.md) to p5.js? (If you're not sure, you can type "Unsure" here and let others from the community offer their thoughts.) + validations: + required: true + - type: checkboxes + id: sub-area + attributes: + label: Most appropriate sub-area of p5.js? + description: You may select more than one. + options: + - label: Accessibility + - label: Color + - label: Core/Environment/Rendering + - label: Data + - label: DOM + - label: Events + - label: Image + - label: IO + - label: Math + - label: Typography + - label: Utilities + - label: WebGL + - label: Build Process + - label: Unit Testing + - label: Internalization + - label: Friendly Errors + - label: Other (specify if possible) + - type: textarea + attributes: + label: Feature request details + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/found-a-bug.yml b/.github/ISSUE_TEMPLATE/found-a-bug.yml index 1286249b6b..12cff53559 100644 --- a/.github/ISSUE_TEMPLATE/found-a-bug.yml +++ b/.github/ISSUE_TEMPLATE/found-a-bug.yml @@ -1,6 +1,6 @@ name: πŸ› Found a Bug description: This template is for reporting bugs (broken or incorrect behaviour). If you have questions about your own code, please visit our forum discourse.processing.org instead. -labels: [bug] +labels: [Bug] body: - type: checkboxes id: sub-area @@ -8,23 +8,23 @@ body: label: Most appropriate sub-area of p5.js? description: You may select more than one. options: - - label: Accessibility (Web Accessibility) - - label: Build tools and processes - - label: Color - - label: Core/Environment/Rendering - - label: Data - - label: DOM - - label: Events - - label: Friendly error system - - label: Image - - label: IO (Input/Output) - - label: Localization - - label: Math - - label: Unit Testing - - label: Typography - - label: Utilities - - label: WebGL - - label: Other (specify if possible) + - label: Accessibility + - label: Color + - label: Core/Environment/Rendering + - label: Data + - label: DOM + - label: Events + - label: Image + - label: IO + - label: Math + - label: Typography + - label: Utilities + - label: WebGL + - label: Build Process + - label: Unit Testing + - label: Internalization + - label: Friendly Errors + - label: Other (specify if possible) - type: input attributes: label: p5.js version diff --git a/.github/config.yml b/.github/config.yml index 706ea19698..c4629c14ed 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -4,8 +4,7 @@ # Comment to be posted to on first time issues newIssueWelcomeComment: > - Welcome! πŸ‘‹ Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, be sure to follow the issue template if you haven't already. - + Welcome! πŸ‘‹ Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, please make sure to fill out the inputs in the issue forms. Thank you! # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome # Comment to be posted to on PRs from first time contributors in your repository diff --git a/.github/labeler.yml b/.github/labeler.yml index 70b33005f7..2cd071249f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,12 +1,32 @@ -# Number of labels to fetch (optional). Defaults to 20 -numLabels: 20 -# These labels will not be used even if the issue contains them -excludeLabels: - - bug - - can't reproduce - - known issue - - more info needed - - will not fix - - severity:critical - - severity:major - - severity:minor +"Area:Accessibility": + - '\[[xX]\]\s*Accessibility' +"Area:Color": + - '\[[xX]\]\s*Color' +"Area:Core": + - '\[[xX]\]\s*Core' +"Area:Data": + - '\[[xX]\]\s*Data' +"Area:DOM": + - '\[[xX]\]\s*DOM' +"Area:Events": + - '\[[xX]\]\s*Events' +"Area:Image": + - '\[[xX]\]\s*Image' +"Area:IO": + - '\[[xX]\]\s*IO' +"Area:Math": + - '\[[xX]\]\s*Math' +"Area:Typography": + - '\[[xX]\]\s*Typography' +"Area:Utilities": + - '\[[xX]\]\s*Utilities' +"Area:WebGL": + - '\[[xX]\]\s*WebGL' +"Build Process": + - '\[[xX]\]\s*Build Process' +"Unit Testing": + - '\[[xX]\]\s*Unit Testing' +"Internalization": + - '\[[xX]\]\s*Internalization' +"Friendly Errors": + - '\[[xX]\]\s*Friendly Errors' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..48d99cfd86 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,15 @@ +name: "Issue Labeler" +on: + issues: + types: [opened, edited] +permissions: + issues: write +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: github/issue-labeler@v2.0 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/labeler.yml + enable-versioned-regex: 0 diff --git a/Gruntfile.js b/Gruntfile.js index 2953b17d17..5ea366e622 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -150,7 +150,7 @@ module.exports = grunt => { source: { options: { parserOptions: { - ecmaVersion: 5 + ecmaVersion: 8 } }, src: ['src/**/*.js'] @@ -163,7 +163,7 @@ module.exports = grunt => { 'eslint-samples': { options: { parserOptions: { - ecmaVersion: 6 + ecmaVersion: 8 }, format: 'unix' }, @@ -259,6 +259,16 @@ module.exports = grunt => { } } }, + babel: { + options: { + presets: ['@babel/preset-env'] + }, + dist: { + files: { + 'lib/p5.pre-min.js': 'lib/p5.js' + } + } + }, // This minifies the javascript into a single file and adds a banner to the // front of the file. @@ -274,8 +284,8 @@ module.exports = grunt => { }, dist: { files: { - 'lib/p5.min.js': 'lib/p5.pre-min.js', - 'lib/modules/p5Custom.min.js': 'lib/modules/p5Custom.pre-min.js' + 'lib/p5.min.js': ['lib/p5.pre-min.js'], + 'lib/modules/p5Custom.min.js': ['lib/modules/p5Custom.pre-min.js'] } } }, @@ -365,7 +375,19 @@ module.exports = grunt => { options: { archive: 'release/p5.zip' }, - files: [{ cwd: 'lib/', src: ['**/*'], expand: true }] + files: [ + { + cwd: 'lib/', + src: [ + 'p5.js', + 'p5.min.js', + 'addons/*', + 'empty-example/*', + 'README.txt' + ], + expand: true + } + ] } }, @@ -511,10 +533,14 @@ module.exports = grunt => { grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-simple-nyc'); + //this library converts the ES6 JS to ES5 so it can be properly minified + grunt.loadNpmTasks('grunt-babel'); + // Create the multitasks. grunt.registerTask('build', [ 'browserify', 'browserify:min', + 'babel', 'uglify', 'browserify:test' ]); diff --git a/README.md b/README.md index a2b4ce7670..6cad85a68d 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ Stewards are contributors that are particularly involved, familiar, or responsiv Anyone interested can volunteer to be a steward! There are no specific requirements for expertise, just an interest in actively learning and participating. If you’re familiar with one or more parts of this project, open an issue to volunteer as a steward! -* [@outofambit](https://github.com/outofambit) - project co-lead -* [@qianqianye](https://github.com/qianqianye) - project co-lead +* [@qianqianye](https://github.com/qianqianye) - p5.js Project Lead +* [@outofambit](https://github.com/outofambit) - p5.js Mentor * [@lmccart](https://github.com/lmccart) * [@limzykenneth](https://github.com/limzykenneth) * [@stalgiag](https://github.com/stalgiag) @@ -63,24 +63,25 @@ Anyone interested can volunteer to be a steward! There are no specific requireme * [@dhowe](https://github.com/dhowe) * [@rahulm2310](https://github.com/rahulm2310) -| Area | Steward(s) | -| :-------------------------------- | :------------------------------------------- | -| Accessibility (Web Accessibility) | outofambit | -| Color | outofambit | -| Core/Environment/Rendering | outofambit
limzykenneth | -| Data | | -| DOM | outofambit | -| Events | outofambit
limzykenneth | -| Image | stalgiag | -| IO | limzykenneth | -| Math | limzykenneth | -| Typography | dhowe | -| Utilities | | -| WebGL | stalgiag | -| Build Process/Unit Testing | outofambit | -| Localization Tools | outofambit | -| Friendly Errors | outofambit | -| [Website](https://github.com/processing/p5.js-website) | limzykenneth
rahulm2310 | +| Area | Steward(s) | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| Overall | [@qianqianye](https://github.com/qianqianye) | +| [Accessibility](https://github.com/processing/p5.js/tree/main/src/accessibility) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | +| [Color](https://github.com/processing/p5.js/tree/main/src/color) | [@KleoP](https://github.com/KleoP), [@murilopolese](https://github.com/murilopolese), [@aahdee](https://github.com/aahdee), [@paulaxisabel](https://github.com/paulaxisabel) | +| [Core](https://github.com/processing/p5.js/tree/main/src/core)/Environment/Rendering | [@limzykenneth](https://github.com/limzykenneth), [@davepagurek](https://github.com/davepagurek), [@jeffawang](https://github.com/jeffawang) | +| [Data](https://github.com/processing/p5.js/tree/main/src/data) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | +| [DOM](https://github.com/processing/p5.js/tree/main/src/dom) | [@outofambit](https://github.com/outofambit), [@SarveshLimaye](https://github.com/SarveshLimaye), [@SamirDhoke](https://github.com/SamirDhoke) | +| [Events](https://github.com/processing/p5.js/tree/main/src/events) | [@limzykenneth](https://github.com/limzykenneth) | +| [Image](https://github.com/processing/p5.js/tree/main/src/image) | [@stalgiag](https://github.com/stalgiag), [@cgusb](https://github.com/cgusb), [@photon-niko](https://github.com/photon-niko), [@KleoP](https://github.com/KleoP) +| [IO](https://github.com/processing/p5.js/tree/main/src/io) | [@limzykenneth](https://github.com/limzykenneth) | +| [Math](https://github.com/processing/p5.js/tree/main/src/math) | [@limzykenneth](https://github.com/limzykenneth), [@jeffawang](https://github.com/jeffawang), [@AdilRabbani](https://github.com/AdilRabbani) | +| [Typography](https://github.com/processing/p5.js/tree/main/src/typography) | [@dhowe](https://github.com/dhowe), [@SarveshLimaye](https://github.com/SarveshLimaye), [@paulaxisabel](https://github.com/paulaxisabel) | +| [Utilities](https://github.com/processing/p5.js/tree/main/src/utilities) | [@kungfuchicken](https://github.com/kungfuchicken), [@cosmicbhejafry](https://github.com/cosmicbhejafry) | +| [WebGL](https://github.com/processing/p5.js/tree/main/src/webgl) | [@stalgiag](https://github.com/stalgiag); GSoC 2022: [@aceslowman](https://github.com/aceslowman)(Contributor), [@kjhollen](https://github.com/kjhollen)(Mentor); [@ShenpaiSharma](https://github.com/ShenpaiSharma)(Contributor), [@calebfoss](https://github.com/calebfoss)(Mentor); [@davepagurek](https://github.com/davepagurek); [@jeffawang](https://github.com/jeffawang); [@AdilRabbani](https://github.com/AdilRabbani) | +| Build Process/Unit Testing | [@outofambit](https://github.com/outofambit), [@kungfuchicken](https://github.com/kungfuchicken) | +| Internalization | [@outofambit](https://github.com/outofambit), [@almchung](https://github.com/almchung) | +| Friendly Errors | [@outofambit](https://github.com/outofambit), [@almchung](https://github.com/almchung) | +| [Contributor Docs](https://github.com/processing/p5.js/tree/main/contributor_docs) | [SoD 2022](https://github.com/processing/p5.js/wiki/Season-of-Docs-2022-Organization-Application---p5.js): [@limzykenneth](https://github.com/limzykenneth) | ## Contributors @@ -562,6 +563,25 @@ We recognize all types of contributions. This project follows the [all-contribut
smilee

πŸ’»
CommanderRoot

πŸ’»
Philip Bell

πŸ“– +
tapioca24

πŸ”Œ +
Qianqian Ye

πŸ’» 🎨 πŸ“– πŸ“‹ πŸ‘€ 🌍 +
Adarsh

🌍 +
kaabe1

🎨 πŸ“‹ + + +
Seb MΓ©ndez

🌍 +
Ryuya

πŸ› πŸ‘€ πŸ’» +
LEMIBANDDEXARI

🌍 +
Vivek Tiwari

🌍 +
Kevin Grajeda

πŸ’» +
anniezhengg

πŸ’» 🎨 +
Seung-Gi Kim(David)

🌍 + + +
Ike Bischof

πŸ’» +
Ong Zhi Zheng

πŸ”Œ +
bsubbaraman

πŸ”Œ +
Jenna deBoisblanc

πŸ”Œ diff --git a/contributor_docs/README.md b/contributor_docs/README.md index 56f4de4984..efb24968c4 100644 --- a/contributor_docs/README.md +++ b/contributor_docs/README.md @@ -2,23 +2,64 @@ # 🌸 Welcome! 🌺 -Thanks for your interest in contributing to p5.js! Our community values contributions of all forms and seeks to expand the meaning of the word "contributor" as far and wide as possible. It includes documentation, teaching, writing code, making art, writing, design, activism, organizing, curating, or anything else you might imagine. [Our community page](https://p5js.org/community/#contribute) gives an overview of some different ways to get involved and contribute. For technical contributions, read on to get started. +Thanks for your interest in contributing to p5.js! Our community values contributions of all forms and seeks to expand the meaning of the word "contributor" as far and wide as possible. It includes documentation, teaching, writing code, making art, writing, design, activism, organizing, curating, or anything else you might imagine. [Our community page](https://p5js.org/community/#contribute) gives an overview of some different ways to get involved and contribute. -This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Add yourself to the [readme](https://github.com/processing/p5.js/blob/main/README.md#contributors) by following the [instructions here](https://github.com/processing/p5.js/issues/2309)! Or comment in the [GitHub issues](https://github.com/processing/p5.js/issues) with your contribution and we'll add you. +This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. We use the @all-contributors bot to handle adding people to the README.md file. You can ask @all-contributors bot to add you in an issue or PR comment like so: +``` +@all-contributors please add @[your github handle] for [your contribution type] +``` +You can find relevant contribution type [here](https://allcontributors.org/docs/en/emoji-key). Although we will usually automatically add you to the contributor list using the bot after merging your PR. The contributor docs are published on p5.js [website](https://p5js.org/contributor-docs/#/), and hosted on p5.js [GitHub repository](https://github.com/processing/p5.js/tree/main/contributor_docs). + +# Before Contributing +Contributing to p5.js should be a stress free experience and we welcome contributions of all levels, whether you are just fixing a small typo in the documentation or refactoring complex 3D rendering functionalities. However there are just a few things you should be familiar with before starting your contribution. + +First, please have a read through our [community statement](https://p5js.org/community/). + +Next, we are currently prioritizing work that expands access (inclusion and accessibility) to p5.js! See [our access statement](./access.md) for more details. + +# Get Started +Now you are ready to start contributing to p5.js! There are many ways to get started with contributing to p5.js and many reasons to do so. For the purpose of this documentation, we will split contributions roughly into two categories. +- Contributions that directly deals with the source code (including documentation) +- Contributions that directly deals with the source code very little or not at all + +Depending on what kind of contribution you are making to p5.js, please read on to the relevant section of this documentation. -# Prioritizing access +## Source code contribution +For a typical contribution to the p5.js or p5.js-website repository, we will follow the following steps: +1. Open an issue +2. Discuss +3. Approved for opening a Pull Request (PR) +4. Make necessary changes +5. Open a PR +6. Discuss +7. Approved and merged -We are prioritizing work that expands access (inclusion and accessibility) to p5.js! See [our access statement](./access.md) for more details. +Head over to [this link](./contributor_guidelines.md) where you will be guided one step at a time on how to navigate the steps above, or you can also use the table of contents on the same page to skip to a relevant part you need a refresher on. + +Most of the time we will stick with this workflow quite strictly and, especially if you have contributed to other projects before, it may feel like there are too many hoops to jump through for what may be a simple contribution. However, the steps above are aimed to make it easy for you as a contributor and for stewards/maintainers to contribute meaningfully, while also making sure that you won't be spending time working on things that may not be accepted for various reasons. The steps above will help ensure that any proposals or fixes are adequately discussed and considered before any work begin, and often this will actually save you (and the steward/maintainer) time because the PR that would need additional fixing after review, or outright not accepted, would happen less often as a result. + +We see contributing to p5.js as a learning opportunity and we don't measure sucess by only looking at the volume of contributions we received. There is no time limit on how long it takes you to complete a contribution, so take your time and work at your own pace. Ask for help from any of the stewards or maintainers if you need them and we'll try our best to support you. + +## Non-source code contribution +There are many more ways to contribute to p5.js through non-source code contribution than can be exhaustively list here, some of the ways may also involve working with some of the p5.js repositories (such as adding example, writing tutorial for the website, etc). Depending on what the planned contribution is, we may be able to support you in different ways so do reach out to us via any channel available to you (email, social media, Discourse forum, Discord, etc). + +## Stewards and maintainers +This section links to different topics related to the general maintenance of p5.js' repositories. +- Responding to issues and reviewing PRs +- How the library is built +- Releasing a new version + +--- # Where our code lives The overarching p5.js project includes some repositories other than this one: -- **[p5.js](https://github.com/processing/p5.js)**: This repository contains the source code for the p5.js library. The [user-facing p5.js reference manual](https://p5js.org/reference/) is also generated from the [JSDoc](http://usejsdoc.org/) comments included in this source code. It is maintained by [Qianqian Q Ye](https://github.com/qianqianye) and [evelyn masso](https://github.com/outofambit). -- **[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 [Kenneth Lim](https://github.com/limzykenneth), [Qianqian Q Ye](https://github.com/qianqianye) and [evelyn masso](https://github.com/outofambit). +- **[p5.js](https://github.com/processing/p5.js)**: This repository contains the source code for the p5.js library. The [user-facing p5.js reference manual](https://p5js.org/reference/) is also generated from the [JSDoc](https://jsdoc.app/) comments included in this source code. It is maintained by [Qianqian Ye](https://github.com/qianqianye) and a group of [stewards](https://github.com/processing/p5.js#stewards). +- **[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 [Qianqian Ye](https://github.com/qianqianye), [Kenneth Lim](https://github.com/limzykenneth), and a group of [stewards](https://github.com/processing/p5.js-website#stewards). - **[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.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). +- Other add-on libraries not listed above usually have their own repository and maintainers and are not maintained by the p5.js project directly. # Repository File Structure diff --git a/contributor_docs/benchmarking_p5.md b/contributor_docs/archive/benchmarking_p5.md similarity index 100% rename from contributor_docs/benchmarking_p5.md rename to contributor_docs/archive/benchmarking_p5.md diff --git a/contributor_docs/discussions.md b/contributor_docs/archive/discussions.md similarity index 100% rename from contributor_docs/discussions.md rename to contributor_docs/archive/discussions.md diff --git a/contributor_docs/es6-adoption.md b/contributor_docs/archive/es6-adoption.md similarity index 100% rename from contributor_docs/es6-adoption.md rename to contributor_docs/archive/es6-adoption.md diff --git a/contributor_docs/roadmap.md b/contributor_docs/archive/roadmap.md similarity index 100% rename from contributor_docs/roadmap.md rename to contributor_docs/archive/roadmap.md diff --git a/contributor_docs/contributing_documentation.md b/contributor_docs/contributing_documentation.md index bc69502a1d..fc52123878 100644 --- a/contributor_docs/contributing_documentation.md +++ b/contributor_docs/contributing_documentation.md @@ -1,3 +1,5 @@ +# Contributing Documentation + Documentation is essential for new learners and experienced programmers alike. It helps make our community inclusive by extending a friendly hand to those who are less familiar with p5.js. It also helps us find the bugs and issues with the code itself, because we test and try things out as we document. There are several ways to contribute to documentation: @@ -31,6 +33,3 @@ While the examples in the reference are meant to be very simplistic snippets of * All discussion happens on github issues, so there's no slack/gitter/etc channel you need to join. * Add your name to the [contributors list](https://github.com/processing/p5.js#contributors) in the readme.md file! Instructions [here](https://github.com/processing/p5.js/issues/2309). * And of course, if you're more of a bug fixer kind of person, feel free to jump into any of the [issues](https://github.com/processing/p5.js/issues)! - -Welcome! We're so glad you're here! -❀️ the p5.js community diff --git a/contributor_docs/contributor_guidelines.md b/contributor_docs/contributor_guidelines.md new file mode 100644 index 0000000000..a2532e74e7 --- /dev/null +++ b/contributor_docs/contributor_guidelines.md @@ -0,0 +1,230 @@ +# Contributor Guidelines +Welcome to the contributor guidelines! This document is for new contributors looking to contribute code to p5.js, contributors looking to refresh their memories on some technical steps, or just about anything else to do with code contributions to p5.js. + +If you are looking to contribute outside of the p5.js repositories (writing tutorials, planning classes, organizing events), please have a look at the other relevant pages instead. Stewards or maintainers may find the [steward guidelines](./steward_guidelines.md) more helpful regarding reviewing issues and pull requests. + +This is a fairly long and comprehensive document but we will try to deliniate all steps and points as clearly as possible. Do utilize the table of contents, the browser search functionality (`Ctrl + f` or `Cmd + f`) to find sections relevant to you. Feel free to skip sections if they are not relevant to your planned contributions as well. + +# Table of Contents +- [All about issues](#all-about-issues) + - [What are issues?](#what-are-issues) + - [Issue templates](#issue-templates) + - [Found a bug](#found-a-bug) + - [Existing Feature Enhancement](#existing-feature-enhancement) + - [New Feature Request](#new-feature-request) + - [Discussion](#discussion) +- [Working on p5.js codebase](#working-on-p5js-codebase) + - [Using the Github edit functionality](#using-the-github-edit-functionality) + - [Forking p5.js and working from your fork](#forking-p5js-and-working-from-your-fork) + - [Codebase breakdown](#codebase-breakdown) + - [Build setup](#build-setup) + - [Git workflow](#git-workflow) + - [Source code](#source-code) + - [Unit tests](#unit-tests) + - [Inline documentation](#inline-documentation) + - [Internationalization](#internationalization) + - [Accessibility](#accessibility) + - [Code standard](#code-standard) + - [Design principles](#design-principles) +- [Pull requests](#pull-requests) + - [Creating a pull request](#creating-a-pull-request) + - [Pull request information](#pull-request-information) + - [Rebase and resolve conflicts](#rebase-and-resolve-conflicts) + - [Discuss and amend](#discuss-and-amend) + +--- +# All about issues +The majority of the activity on p5.js' Github repositories (repo for short) happens in issues and issues will most likely be the place to start your contribution process as well. + +## What are issues? +Issue is the generic name for a post on Github that aims to describe, well, an issue. This "issue" can be a bug report, a request to add new feature, a discussion, a question, an announcement, or anything that works as a post. Comments can be added below each issue by anyone with a Github account, including bots! It is the place where contributors dicusses topics related to the development of the project in the repo. + +While an issue can be opened for a wide variety of reasons, for p5.js' repos we usually only use issues to discuss p5.js source code development related topics. Topics such as debugging your own code, inviting collaborators to your project, or other unrelated topics should be discuss either on the [forum](https://discourse.processing.com) or on other platforms. + +We have created easy to use issue templates to aid you in deciding whether a topic should be a Github issue or it should be posted somewhere else! + +## Issue templates +p5.js' issue templates not only makes it easier for stewards and maintainers to understand and review issues, it also makes it simpler for you to file the relevant issue and receive a reply faster. Although they are called templates, from your perspective, it will just be like filling in a simple form where all the different fields of the form are the potentially important information that issue reviewers will need to properly diagnose your issue. + +To file a new issue, simply go to the "Issues" tab on the p5.js repo and click on the "New issue" button (usually in green and on the right side). Once you have clicked that, you will be presented with several different options, each of which either correspond to a relevant issue template or redirect you to the relevant place to file your question. You should choose the most relevant option out of all that are presented to ensure your issue can receive the right attention promptly. We will cover the issue templates that applies to p5.js below, for other repos, please check their respective contributor documentation. + +### "Found a bug" +When you encounter possible incorrect behaviour in p5.js or something not behaving as described in the documentation, this is the template you should use. Please note that if you are trying to debug your own code or figure out why your sketch is not behaving as you expected and you think it may be a problem with your code, you should ask on the [forum](https://discourse.processing.org) instead. If it is later determined your problem did stem from p5.js, you can always open an issue and use this template then. + +There are few fields for you to fill in for this template: +1. "Most appropriate sub-area of p5.js?" - This helps the appropriate stewards identify and respond to your issue. This will automatically tag the issue with the relevant [labels](./issue_labels.md). +2. "p5.js version" - You can find the p5.js version number in either the ` + diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index de6c862644..69b202ee71 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -4,4 +4,4 @@ function setup() { function draw() { // put drawing code here -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index fbf0059a65..1a0835fbe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "p5", - "version": "1.4.1", + "version": "1.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -6606,6 +6606,12 @@ } } }, + "gifenc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/gifenc/-/gifenc-1.0.3.tgz", + "integrity": "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==", + "dev": true + }, "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -6821,6 +6827,12 @@ } } }, + "grunt-babel": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/grunt-babel/-/grunt-babel-8.0.0.tgz", + "integrity": "sha512-WuiZFvGzcyzlEoPIcY1snI234ydDWeWWV5bpnB7PZsOLHcDsxWKnrR1rMWEUsbdVPPjvIirwFNsuo4CbJmsdFQ==", + "dev": true + }, "grunt-cli": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz", @@ -14105,9 +14117,9 @@ "dev": true }, "shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "dev": true }, "signal-exit": { diff --git a/package.json b/package.json index 76acf7cb9b..532a78fc64 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "node --require @babel/register ./utils/sample-linter.js" ] }, - "version": "1.4.1", + "version": "1.4.2", "devDependencies": { "@babel/core": "^7.7.7", "@babel/preset-env": "^7.10.2", @@ -67,6 +67,7 @@ "grunt-mocha-test": "^0.13.3", "grunt-newer": "^1.1.0", "grunt-simple-nyc": "^3.0.1", + "grunt-babel": "^8.0.0", "html-entities": "^1.3.1", "husky": "^4.2.3", "i18next": "^19.0.2", @@ -85,7 +86,8 @@ "regenerator-runtime": "^0.13.3", "request": "^2.88.0", "simple-git": "^3.3.0", - "whatwg-fetch": "^2.0.4" + "whatwg-fetch": "^2.0.4", + "gifenc": "^1.0.3" }, "license": "LGPL-2.1", "main": "./lib/p5.min.js", @@ -154,7 +156,6 @@ "not dead" ], "author": "", - "dependencies": {}, "husky": { "hooks": { "pre-commit": "lint-staged" diff --git a/src/core/friendly_errors/sketch_reader.js b/src/core/friendly_errors/sketch_reader.js index edace305fc..c9c3159577 100644 --- a/src/core/friendly_errors/sketch_reader.js +++ b/src/core/friendly_errors/sketch_reader.js @@ -116,8 +116,43 @@ if (typeof IS_MINIFIED !== 'undefined') { //these regex are used to perform variable extraction //visit https://regexr.com/ for the detailed view - const varName = /(?:(?:let|const|var)\s+)?([\w$]+)/; - const varNameWithComma = /(?:(?:let|const|var)\s+)?([\w$,]+)/; + const optionalVarKeyword = /(?:(?:let|const|var)\s+)?/; + + // Bracketed expressions start with an opening bracket, some amount of non + // bracket characters, then a closing bracket. Note that this won't properly + // parse nested brackets: `constrain(millis(), 0, 1000)` will match + // `constrain(millis()` only, but will still fail gracefully and not try to + // mistakenly read any subsequent code as assignment expressions. + const roundBracketedExpr = /(?:\([^)]*\))/; + const squareBracketedExpr = /(?:\[[^\]]*\])/; + const curlyBracketedExpr = /(?:\{[^}]*\})/; + const bracketedExpr = new RegExp( + [roundBracketedExpr, squareBracketedExpr, curlyBracketedExpr] + .map(regex => regex.source) + .join('|') + ); + + // In an a = b expression, `b` can be any character up to a newline or comma, + // unless the comma is inside of a bracketed expression of some kind (to make + // sure we parse function calls with multiple arguments properly.) + const rightHandSide = new RegExp('(?:' + bracketedExpr.source + '|[^\\n,])+'); + + const leftHandSide = /([\w$]+)/; + const assignmentOperator = /\s*=\s*/; + const singleAssignment = new RegExp( + leftHandSide.source + assignmentOperator.source + rightHandSide.source + ); + const listSeparator = /,\s*/; + const oneOrMoreAssignments = new RegExp( + '(?:' + + singleAssignment.source + + listSeparator.source + + ')*' + + singleAssignment.source + ); + const assignmentStatement = new RegExp( + '^' + optionalVarKeyword.source + oneOrMoreAssignments.source + ); const letConstName = /(?:(?:let|const)\s+)([\w$]+)/; /** @@ -133,27 +168,12 @@ if (typeof IS_MINIFIED !== 'undefined') { //extract variable names from the user's code let matches = []; linesArray.forEach(ele => { - if (ele.includes(',')) { - matches.push( - ...ele.split(',').flatMap(s => { - //below RegExps extract a, b, c from let/const a=10, b=20, c; - //visit https://regexr.com/ for the detailed view. - let match; - if (s.includes('=')) { - match = s.match(/(\w+)\s*(?==)/i); - if (match !== null) return match[1]; - } else if (!s.match(new RegExp('[[]{}]'))) { - let m = s.match(varName); - if (m !== null) return s.match(varNameWithComma)[1]; - } else return []; - }) - ); - } else { - //extract a from let/const a=10; - //visit https://regexr.com/ for the detailed view. - const match = ele.match(letConstName); - if (match !== null) matches.push(match[1]); - } + // Match 0 is the part of the line of code that the regex looked at. + // Matches 1 and onward will be only the variable names on the left hand + // side of assignment expressions. + const match = ele.match(assignmentStatement); + if (!match) return; + matches.push(...match.slice(1).filter(group => group !== undefined)); }); //check if the obtained variables are a part of p5.js or not checkForConstsAndFuncs(matches); diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 2e0ab55abc..541a316c86 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -233,6 +233,8 @@ p5.Renderer.prototype.text = function(str, x, y, maxWidth, maxHeight) { let chars; let shiftedY; let finalMaxHeight = Number.MAX_VALUE; + // fix for #5785 (top of bounding box) + let finalMinHeight = y; if (!(this._doFill || this._doStroke)) { return; @@ -263,29 +265,48 @@ p5.Renderer.prototype.text = function(str, x, y, maxWidth, maxHeight) { break; } - let baselineHacked = false; if (typeof maxHeight !== 'undefined') { if (this._rectMode === constants.CENTER) { y -= maxHeight / 2; } + let originalY = y; + let ascent = p.textAscent(); + switch (this._textBaseline) { case constants.BOTTOM: shiftedY = y + maxHeight; y = Math.max(shiftedY, y); + // fix for #5785 (top of bounding box) + finalMinHeight += ascent; break; case constants.CENTER: shiftedY = y + maxHeight / 2; y = Math.max(shiftedY, y); - break; - case constants.BASELINE: - baselineHacked = true; - this._textBaseline = constants.TOP; + // fix for #5785 (top of bounding box) + finalMinHeight += ascent / 2; break; } // remember the max-allowed y-position for any line (fix to #928) - finalMaxHeight = y + maxHeight - p.textAscent(); + finalMaxHeight = y + maxHeight - ascent; + + // fix for #5785 (bottom of bounding box) + if (this._textBaseline === constants.CENTER) { + finalMaxHeight = originalY + maxHeight - ascent / 2; + } + } else { + // no text-height specified, show warning for BOTTOM / CENTER + if (this._textBaseline === constants.BOTTOM) { + return console.warn( + 'textAlign(*, BOTTOM) requires x, y, width and height' + ); + } + if (this._textBaseline === constants.CENTER) { + return console.warn( + 'textAlign(*, CENTER) requires x, y, width and height' + ); + } } // Render lines of text according to settings of textWrap @@ -310,10 +331,9 @@ p5.Renderer.prototype.text = function(str, x, y, maxWidth, maxHeight) { } let offset = 0; - const vAlign = p.textAlign().vertical; - if (vAlign === constants.CENTER) { + if (this._textBaseline === constants.CENTER) { offset = (nlines.length - 1) * p.textLeading() / 2; - } else if (vAlign === constants.BOTTOM) { + } else if (this._textBaseline === constants.BOTTOM) { offset = (nlines.length - 1) * p.textLeading(); } @@ -324,18 +344,29 @@ p5.Renderer.prototype.text = function(str, x, y, maxWidth, maxHeight) { testLine = `${line + words[wordIndex]}` + ' '; testWidth = this.textWidth(testLine); if (testWidth > maxWidth && line.length > 0) { - this._renderText(p, line.trim(), x, y - offset, finalMaxHeight); + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); line = `${words[wordIndex]}` + ' '; y += p.textLeading(); } else { line = testLine; } } - this._renderText(p, line.trim(), x, y - offset, finalMaxHeight); + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); y += p.textLeading(); - if (baselineHacked) { - this._textBaseline = constants.BASELINE; - } } } else { let nlines = []; @@ -356,10 +387,9 @@ p5.Renderer.prototype.text = function(str, x, y, maxWidth, maxHeight) { nlines.push(line); let offset = 0; - const vAlign = p.textAlign().vertical; - if (vAlign === constants.CENTER) { + if (this._textBaseline === constants.CENTER) { offset = (nlines.length - 1) * p.textLeading() / 2; - } else if (vAlign === constants.BOTTOM) { + } else if (this._textBaseline === constants.BOTTOM) { offset = (nlines.length - 1) * p.textLeading(); } @@ -374,33 +404,49 @@ p5.Renderer.prototype.text = function(str, x, y, maxWidth, maxHeight) { if (testWidth <= maxWidth) { line += chars[charIndex]; } else if (testWidth > maxWidth && line.length > 0) { - this._renderText(p, line.trim(), x, y - offset, finalMaxHeight); + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); y += p.textLeading(); line = `${chars[charIndex]}`; } } } - this._renderText(p, line.trim(), x, y - offset, finalMaxHeight); + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); y += p.textLeading(); - - if (baselineHacked) { - this._textBaseline = constants.BASELINE; - } } } else { // Offset to account for vertically centering multiple lines of text - no // need to adjust anything for vertical align top or baseline let offset = 0; - const vAlign = p.textAlign().vertical; - if (vAlign === constants.CENTER) { + if (this._textBaseline === constants.CENTER) { offset = (lines.length - 1) * p.textLeading() / 2; - } else if (vAlign === constants.BOTTOM) { + } else if (this._textBaseline === constants.BOTTOM) { offset = (lines.length - 1) * p.textLeading(); } // Renders lines of text at any line breaks present in the original string for (let i = 0; i < lines.length; i++) { - this._renderText(p, lines[i], x, y - offset, finalMaxHeight); + this._renderText( + p, + lines[i], + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); y += p.textLeading(); } } diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 345e7d9cbe..ff8c6dacef 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1,6 +1,5 @@ import p5 from './main'; import * as constants from './constants'; -import filters from '../image/filters'; import './p5.Renderer'; @@ -47,7 +46,14 @@ p5.Renderer2D.prototype.background = function(...args) { this.resetMatrix(); if (args[0] instanceof p5.Image) { - this._pInst.image(args[0], 0, 0, this.width, this.height); + if (args[1] >= 0) { + // set transparency of background + const img = args[0]; + this.drawingContext.globalAlpha = args[1] / 255; + this._pInst.image(img, 0, 0, this.width, this.height); + } else { + this._pInst.image(args[0], 0, 0, this.width, this.height); + } } else { const curFill = this._getFill(); // create background rect @@ -155,13 +161,8 @@ p5.Renderer2D.prototype.image = function( } try { - if (this._tint) { - if (p5.MediaElement && img instanceof p5.MediaElement) { - img.loadPixels(); - } - if (img.canvas) { - cnv = this._getTintedImageCanvas(img); - } + if (this._tint && img.canvas) { + cnv = this._getTintedImageCanvas(img); } if (!cnv) { cnv = img.canvas || img.elt; @@ -198,25 +199,66 @@ p5.Renderer2D.prototype._getTintedImageCanvas = function(img) { if (!img.canvas) { return img; } - const pixels = filters._toPixels(img.canvas); - const tmpCanvas = document.createElement('canvas'); - tmpCanvas.width = img.canvas.width; - tmpCanvas.height = img.canvas.height; - const tmpCtx = tmpCanvas.getContext('2d'); - const id = tmpCtx.createImageData(img.canvas.width, img.canvas.height); - const newPixels = id.data; - for (let i = 0; i < pixels.length; i += 4) { - const r = pixels[i]; - const g = pixels[i + 1]; - const b = pixels[i + 2]; - const a = pixels[i + 3]; - newPixels[i] = r * this._tint[0] / 255; - newPixels[i + 1] = g * this._tint[1] / 255; - newPixels[i + 2] = b * this._tint[2] / 255; - newPixels[i + 3] = a * this._tint[3] / 255; + + if (!img.tintCanvas) { + // Once an image has been tinted, keep its tint canvas + // around so we don't need to re-incur the cost of + // creating a new one for each tint + img.tintCanvas = document.createElement('canvas'); + } + + // Keep the size of the tint canvas up-to-date + if (img.tintCanvas.width !== img.canvas.width) { + img.tintCanvas.width = img.canvas.width; + } + if (img.tintCanvas.height !== img.canvas.height) { + img.tintCanvas.height = img.canvas.height; + } + + // Goal: multiply the r,g,b,a values of the source by + // the r,g,b,a values of the tint color + const ctx = img.tintCanvas.getContext('2d'); + + ctx.save(); + ctx.clearRect(0, 0, img.canvas.width, img.canvas.height); + + if (this._tint[0] < 255 || this._tint[1] < 255 || this._tint[2] < 255) { + // Color tint: we need to use the multiply blend mode to change the colors. + // However, the canvas implementation of this destroys the alpha channel of + // the image. To accommodate, we first get a version of the image with full + // opacity everywhere, tint using multiply, and then use the destination-in + // blend mode to restore the alpha channel again. + + // Start with the original image + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode makes everything opaque but forces the luma to match + // the original image again + ctx.globalCompositeOperation = 'luminosity'; + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode forces the hue and chroma to match the original image. + // After this we should have the original again, but with full opacity. + ctx.globalCompositeOperation = 'color'; + ctx.drawImage(img.canvas, 0, 0); + + // Apply color tint + ctx.globalCompositeOperation = 'multiply'; + ctx.fillStyle = `rgb(${this._tint.slice(0, 3).join(', ')})`; + ctx.fillRect(0, 0, img.canvas.width, img.canvas.height); + + // Replace the alpha channel with the original alpha * the alpha tint + ctx.globalCompositeOperation = 'destination-in'; + ctx.globalAlpha = this._tint[3] / 255; + ctx.drawImage(img.canvas, 0, 0); + } else { + // If we only need to change the alpha, we can skip all the extra work! + ctx.globalAlpha = this._tint[3] / 255; + ctx.drawImage(img.canvas, 0, 0); } - tmpCtx.putImageData(id, 0, 0); - return tmpCanvas; + + ctx.restore(); + return img.tintCanvas; }; ////////////////////////////////////////////// @@ -1148,9 +1190,9 @@ p5.Renderer2D.prototype.text = function(str, x, y, maxWidth, maxHeight) { return p; }; -p5.Renderer2D.prototype._renderText = function(p, line, x, y, maxY) { - if (y >= maxY) { - return; // don't render lines beyond our maxY position +p5.Renderer2D.prototype._renderText = function(p, line, x, y, maxY, minY) { + if (y < minY || y >= maxY) { + return; // don't render lines beyond our minY/maxY bounds (see #5785) } p.push(); // fix to #803 diff --git a/src/core/rendering.js b/src/core/rendering.js index c2526ad726..391987a409 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -252,7 +252,8 @@ p5.prototype.createGraphics = function(w, h, renderer) { * min(A*factor, B). *
  • LIGHTEST - only the lightest colour succeeds: C = * max(A*factor, B).
  • - *
  • DIFFERENCE - subtract colors from underlying image.
  • + *
  • DIFFERENCE - subtract colors from underlying image. + * (2D)
  • *
  • EXCLUSION - similar to DIFFERENCE, but less * extreme.
  • *
  • MULTIPLY - multiply the colors, result will always be diff --git a/src/core/shape/2d_primitives.js b/src/core/shape/2d_primitives.js index 7484433b16..841156e53c 100644 --- a/src/core/shape/2d_primitives.js +++ b/src/core/shape/2d_primitives.js @@ -370,7 +370,7 @@ p5.prototype._renderEllipse = function(x, y, w, h, detailX) { * stroke(255); * line(85, 75, 30, 75); * describe( - * '3 lines of various stroke sizes. Form top, bottom and right sides of a square' + * '3 lines of various stroke colors. Form top, bottom and right sides of a square' * ); * * diff --git a/src/core/shape/attributes.js b/src/core/shape/attributes.js index bf23b9c58e..84d6158bd3 100644 --- a/src/core/shape/attributes.js +++ b/src/core/shape/attributes.js @@ -103,11 +103,12 @@ p5.prototype.ellipseMode = function(m) { * 2 pixelated 36Γ—36 white ellipses to left & right of center, black background */ p5.prototype.noSmooth = function() { - this.setAttributes('antialias', false); if (!this._renderer.isP3D) { if ('imageSmoothingEnabled' in this.drawingContext) { this.drawingContext.imageSmoothingEnabled = false; } + } else { + this.setAttributes('antialias', false); } return this; }; diff --git a/src/core/transform.js b/src/core/transform.js index f77523ab64..b14d14e7a9 100644 --- a/src/core/transform.js +++ b/src/core/transform.js @@ -30,36 +30,6 @@ import p5 from './main'; * @method applyMatrix * @param {Array} arr an array of numbers - should be 6 or 16 length (2*3 or 4*4 matrix values) * @chainable - */ -/** - * @method applyMatrix - * @param {Number} a numbers which define the 2Γ—3 or 4x4 matrix to be multiplied - * @param {Number} b numbers which define the 2Γ—3 or 4x4 matrix to be multiplied - * @param {Number} c numbers which define the 2Γ—3 or 4x4 matrix to be multiplied - * @param {Number} d numbers which define the 2Γ—3 or 4x4 matrix to be multiplied - * @param {Number} e numbers which define the 2Γ—3 or 4x4 matrix to be multiplied - * @param {Number} f numbers which define the 2Γ—3 or 4x4 matrix to be multiplied - * @chainable - */ -/** - * @method applyMatrix - * @param {Number} a - * @param {Number} b - * @param {Number} c - * @param {Number} d - * @param {Number} e - * @param {Number} f - * @param {Number} g numbers which define the 4x4 matrix to be multiplied - * @param {Number} h numbers which define the 4x4 matrix to be multiplied - * @param {Number} i numbers which define the 4x4 matrix to be multiplied - * @param {Number} j numbers which define the 4x4 matrix to be multiplied - * @param {Number} k numbers which define the 4x4 matrix to be multiplied - * @param {Number} l numbers which define the 4x4 matrix to be multiplied - * @param {Number} m numbers which define the 4x4 matrix to be multiplied - * @param {Number} n numbers which define the 4x4 matrix to be multiplied - * @param {Number} o numbers which define the 4x4 matrix to be multiplied - * @param {Number} p numbers which define the 4x4 matrix to be multiplied - * @chainable * @example *
    * @@ -183,6 +153,36 @@ import p5 from './main'; * A rectangle shearing * A rectangle in the upper left corner */ +/** + * @method applyMatrix + * @param {Number} a numbers which define the 2Γ—3 or 4x4 matrix to be multiplied + * @param {Number} b numbers which define the 2Γ—3 or 4x4 matrix to be multiplied + * @param {Number} c numbers which define the 2Γ—3 or 4x4 matrix to be multiplied + * @param {Number} d numbers which define the 2Γ—3 or 4x4 matrix to be multiplied + * @param {Number} e numbers which define the 2Γ—3 or 4x4 matrix to be multiplied + * @param {Number} f numbers which define the 2Γ—3 or 4x4 matrix to be multiplied + * @chainable + */ +/** + * @method applyMatrix + * @param {Number} a + * @param {Number} b + * @param {Number} c + * @param {Number} d + * @param {Number} e + * @param {Number} f + * @param {Number} g numbers which define the 4x4 matrix to be multiplied + * @param {Number} h numbers which define the 4x4 matrix to be multiplied + * @param {Number} i numbers which define the 4x4 matrix to be multiplied + * @param {Number} j numbers which define the 4x4 matrix to be multiplied + * @param {Number} k numbers which define the 4x4 matrix to be multiplied + * @param {Number} l numbers which define the 4x4 matrix to be multiplied + * @param {Number} m numbers which define the 4x4 matrix to be multiplied + * @param {Number} n numbers which define the 4x4 matrix to be multiplied + * @param {Number} o numbers which define the 4x4 matrix to be multiplied + * @param {Number} p numbers which define the 4x4 matrix to be multiplied + * @chainable + */ p5.prototype.applyMatrix = function() { let isTypedArray = arguments[0] instanceof Object.getPrototypeOf(Uint8Array); if (Array.isArray(arguments[0]) || isTypedArray) { diff --git a/src/dom/dom.js b/src/dom/dom.js index a07e277370..af53e34793 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -622,8 +622,8 @@ p5.prototype.createCheckbox = function() { /** * Creates a dropdown menu `<select></select>` element in the DOM. - * It also helps to assign select-box methods to p5.Element when selecting existing select box. - * - `.option(name, [value])` can be used to set options for the select after it is created. + * It also assigns select-related methods to p5.Element when selecting an existing select box. Options in the menu are unique by `name` (the display text). + * - `.option(name, [value])` can be used to add an option with `name` (the display text) and `value` to the select element. If an option with `name` already exists within the select element, this method will change its value to `value`. * - `.value()` will return the currently selected option. * - `.selected()` will return the current dropdown element which is an instance of p5.Element. * - `.selected(value)` can be used to make given option selected by default when the page first loads. @@ -844,19 +844,32 @@ p5.prototype.createRadio = function() { // If already given with a containerEl, will search for all input[radio] // it, create a p5.Element out of it, add options to it and return the p5.Element. + let self; let radioElement; let name; const arg0 = arguments[0]; - // If existing radio Element is provided as argument 0 - if (arg0 instanceof HTMLDivElement || arg0 instanceof HTMLSpanElement) { + if ( + arg0 instanceof p5.Element && + (arg0.elt instanceof HTMLDivElement || arg0.elt instanceof HTMLSpanElement) + ) { + // If given argument is p5.Element of div/span type + self = arg0; + this.elt = arg0.elt; + } else if ( + // If existing radio Element is provided as argument 0 + arg0 instanceof HTMLDivElement || + arg0 instanceof HTMLSpanElement + ) { + self = addElement(arg0, this); + this.elt = arg0; radioElement = arg0; if (typeof arguments[1] === 'string') name = arguments[1]; } else { if (typeof arg0 === 'string') name = arg0; radioElement = document.createElement('div'); + self = addElement(radioElement, this); + this.elt = radioElement; } - this.elt = radioElement; - let self = addElement(radioElement, this); self._name = name || 'radioOption'; // setup member functions @@ -1305,17 +1318,8 @@ p5.prototype.createAudio = function(src, callback) { /** CAMERA STUFF **/ -/** - * @property {String} VIDEO - * @final - * @category Constants - */ p5.prototype.VIDEO = 'video'; -/** - * @property {String} AUDIO - * @final - * @category Constants - */ + p5.prototype.AUDIO = 'audio'; // from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia @@ -1980,8 +1984,8 @@ p5.Element.prototype.style = function(prop, val) { ) { let styles = window.getComputedStyle(self.elt); let styleVal = styles.getPropertyValue(prop); - let numVal = styleVal.replace(/\D+/g, ''); - this[prop] = parseInt(numVal, 10); + let numVal = styleVal.replace(/[^\d.]/g, ''); + this[prop] = Math.round(parseFloat(numVal, 10)); } } return this; diff --git a/src/image/image.js b/src/image/image.js index 8260776c13..d2f0d82c99 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -184,7 +184,10 @@ p5.prototype.saveCanvas = function() { }, mimeType); }; -p5.prototype.saveGif = function(pImg, filename) { +// this is the old saveGif, left here for compatibility purposes +// the only place I found it being used was on image/p5.Image.js, on the +// save function. that has been changed to use this function. +p5.prototype.encodeAndDownloadGif = function(pImg, filename) { const props = pImg.gifProperties; //convert loopLimit back into Netscape Block formatting @@ -420,15 +423,19 @@ p5.prototype.saveGif = function(pImg, filename) { * as an argument to the callback function as an array of objects, with the * size of array equal to the total number of frames. * - * Note that saveFrames() will only save the first 15 frames of an animation. + * The arguments `duration` and `framerate` are constrained to be less or equal to 15 and 22, respectively, which means you + * can only download a maximum of 15 seconds worth of frames at 22 frames per second, adding up to 330 frames. + * This is done in order to avoid memory problems since a large enough canvas can fill up the memory in your computer + * very easily and crash your program or even your browser. + * * To export longer animations, you might look into a library like * ccapture.js. * * @method saveFrames * @param {String} filename * @param {String} extension 'jpg' or 'png' - * @param {Number} duration Duration in seconds to save the frames for. - * @param {Number} framerate Framerate to save the frames in. + * @param {Number} duration Duration in seconds to save the frames for. This parameter will be constrained to be less or equal to 15. + * @param {Number} framerate Framerate to save the frames in. This parameter will be constrained to be less or equal to 22. * @param {function(Array)} [callback] A callback function that will be executed to handle the image data. This function should accept an array as argument. The diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 3ff2b3f053..eb125d8309 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -6,10 +6,10 @@ */ import p5 from '../core/main'; -import Filters from './filters'; import canvas from '../core/helpers'; import * as constants from '../core/constants'; import omggif from 'omggif'; +import { GIFEncoder, quantize, applyPalette } from 'gifenc'; import '../core/friendly_errors/validate_params'; import '../core/friendly_errors/file_errors'; @@ -159,6 +159,391 @@ p5.prototype.loadImage = function(path, successCallback, failureCallback) { return pImg; }; +/** + * Generates a gif of your current animation and downloads it to your computer! + * + * The duration argument specifies how many seconds you want to record from your animation. + * This value is then converted to the necessary number of frames to generate it, depending + * on the value of units. More on that on the next paragraph. + * + * An optional object that can contain two more arguments: delay (number) and units (string). + * + * `delay`, specifying how much time we should wait before recording + * + * `units`, a string that can be either 'seconds' or 'frames'. By default it's 'seconds'. + * + * `units` specifies how the duration and delay arguments will behave. + * If 'seconds', these arguments will correspond to seconds, meaning that 3 seconds worth of animation + * will be created. If 'frames', the arguments now correspond to the number of frames you want your + * animation to be, if you are very sure of this number. + * + * It is not recommended to write this function inside setup, since it won't work properly. + * The recommended use can be seen in the example, where we use it inside an event function, + * like keyPressed or mousePressed. + * + * @method saveGif + * @param {String} filename File name of your gif + * @param {Number} duration Duration in seconds that you wish to capture from your sketch + * @param {Object} options An optional object that can contain two more arguments: delay, specifying + * how much time we should wait before recording, and units, a string that can be either 'seconds' or + * 'frames'. By default it's 'seconds'. + * + * @example + *
    + * + * function setup() { + * createCanvas(100, 100); + * } + * + * function draw() { + * colorMode(RGB); + * background(30); + * + * // create a bunch of circles that move in... circles! + * for (let i = 0; i < 10; i++) { + * let opacity = map(i, 0, 10, 0, 255); + * noStroke(); + * fill(230, 250, 90, opacity); + * circle( + * 30 * sin(frameCount / (30 - i)) + width / 2, + * 30 * cos(frameCount / (30 - i)) + height / 2, + * 10 + * ); + * } + * } + * + * // you can put it in the mousePressed function, + * // or keyPressed for example + * function keyPressed() { + * // this will download the first 5 seconds of the animation! + * if (key === 's') { + * saveGif('mySketch', 5); + * } + * } + * + *
    + * + * @alt + * animation of a group of yellow circles moving in circles over a dark background + */ +p5.prototype.saveGif = async function( + fileName, + duration, + options = { delay: 0, units: 'seconds' } +) { + // validate parameters + if (typeof fileName !== 'string') { + throw TypeError('fileName parameter must be a string'); + } + if (typeof duration !== 'number') { + throw TypeError('Duration parameter must be a number'); + } + // if arguments in the options object are not correct, cancel operation + if (typeof options.delay !== 'number') { + throw TypeError('Delay parameter must be a number'); + } + // if units is not seconds nor frames, throw error + if (options.units !== 'seconds' && options.units !== 'frames') { + throw TypeError('Units parameter must be either "frames" or "seconds"'); + } + + // extract variables for more comfortable use + let units = options.units; + let delay = options.delay; + + // console.log(options); + + // get the project's framerate + let _frameRate = this._targetFrameRate; + // if it is undefined or some non useful value, assume it's 60 + if (_frameRate === Infinity || _frameRate === undefined || _frameRate === 0) { + _frameRate = 60; + } + + // calculate frame delay based on frameRate + + // this delay has nothing to do with the + // delay in options, but rather is the delay + // we have to specify to the gif encoder between frames. + let gifFrameDelay = 1 / _frameRate * 1000; + + // constrain it to be always greater than 20, + // otherwise it won't work in some browsers and systems + // reference: https://stackoverflow.com/questions/64473278/gif-frame-duration-seems-slower-than-expected + gifFrameDelay = gifFrameDelay < 20 ? 20 : gifFrameDelay; + + // check the mode we are in and how many frames + // that duration translates to + const nFrames = units === 'seconds' ? duration * _frameRate : duration; + const nFramesDelay = units === 'seconds' ? delay * _frameRate : delay; + const totalNumberOfFrames = nFrames + nFramesDelay; + + // initialize variables for the frames processing + let frameIterator = nFramesDelay; + this.frameCount = frameIterator; + + const lastPixelDensity = this._pixelDensity; + this.pixelDensity(1); + + // We first take every frame that we are going to use for the animation + let frames = []; + + let progressBarIdName = 'p5.gif.progressBar'; + if (document.getElementById(progressBarIdName) !== null) + document.getElementById(progressBarIdName).remove(); + + let p = this.createP(''); + p.id('progressBar'); + + p.style('font-size', '16px'); + p.style('font-family', 'Montserrat'); + p.style('background-color', '#ffffffa0'); + p.style('padding', '8px'); + p.style('border-radius', '10px'); + p.position(0, 0); + + let pixels; + let gl; + if (this.drawingContext instanceof WebGLRenderingContext) { + // if we have a WEBGL context, initialize the pixels array + // and the gl context to use them inside the loop + gl = document.getElementById('defaultCanvas0').getContext('webgl'); + pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4); + } + + // stop the loop since we are going to manually redraw + this.noLoop(); + + while (frameIterator < totalNumberOfFrames) { + /* + we draw the next frame. this is important, since + busy sketches or low end devices might take longer + to render some frames. So we just wait for the frame + to be drawn and immediately save it to a buffer and continue + */ + this.redraw(); + + // depending on the context we'll extract the pixels one way + // or another + let data = undefined; + + if (this.drawingContext instanceof WebGLRenderingContext) { + pixels = new Uint8Array( + gl.drawingBufferWidth * gl.drawingBufferHeight * 4 + ); + gl.readPixels( + 0, + 0, + gl.drawingBufferWidth, + gl.drawingBufferHeight, + gl.RGBA, + gl.UNSIGNED_BYTE, + pixels + ); + + data = _flipPixels(pixels); + } else { + data = this.drawingContext.getImageData(0, 0, this.width, this.height) + .data; + } + + frames.push(data); + frameIterator++; + + p.html( + 'Saved frame ' + + frames.length.toString() + + ' out of ' + + nFrames.toString() + ); + await new Promise(resolve => setTimeout(resolve, 0)); + } + p.html('Frames processed, generating color palette...'); + + this.loop(); + this.pixelDensity(lastPixelDensity); + + // create the gif encoder and the colorspace format + const gif = GIFEncoder(); + + // calculate the global palette for this set of frames + const globalPalette = _generateGlobalPalette(frames); + + // the way we designed the palette means we always take the last index for transparency + const transparentIndex = globalPalette.length - 1; + + // we are going to iterate the frames in pairs, n-1 and n + for (let i = 0; i < frames.length; i++) { + if (i === 0) { + const indexedFrame = applyPalette(frames[i], globalPalette, { + format: 'rgba4444' + }); + gif.writeFrame(indexedFrame, this.width, this.height, { + palette: globalPalette, + delay: gifFrameDelay, + dispose: 1 + }); + continue; + } + + // matching pixels between frames can be set to full transparency, + // kinda digging a "hole" into the frame to see the pixels that where behind it + // (which would be the exact same, so not noticeable changes) + // this helps make the file quite smaller + let currFramePixels = frames[i]; + let lastFramePixels = frames[i - 1]; + let matchingPixelsInFrames = []; + for (let p = 0; p < currFramePixels.length; p += 4) { + let currPixel = [ + currFramePixels[p], + currFramePixels[p + 1], + currFramePixels[p + 2], + currFramePixels[p + 3] + ]; + let lastPixel = [ + lastFramePixels[p], + lastFramePixels[p + 1], + lastFramePixels[p + 2], + lastFramePixels[p + 3] + ]; + + // if the pixels are equal, save this index to be used later + if (_pixelEquals(currPixel, lastPixel)) { + matchingPixelsInFrames.push(p / 4); + } + } + // we decide on one of this colors to be fully transparent + // Apply palette to RGBA data to get an indexed bitmap + const indexedFrame = applyPalette(currFramePixels, globalPalette, { + format: 'rgba4444' + }); + + for (let i = 0; i < matchingPixelsInFrames.length; i++) { + // here, we overwrite whatever color this pixel was assigned to + // with the color that we decided we are going to use as transparent. + // down in writeFrame we are going to tell the encoder that whenever + // it runs into "transparentIndex", just dig a hole there allowing to + // see through what was in the frame before it. + let pixelIndex = matchingPixelsInFrames[i]; + indexedFrame[pixelIndex] = transparentIndex; + } + + // Write frame into the encoder + gif.writeFrame(indexedFrame, this.width, this.height, { + delay: gifFrameDelay, + transparent: true, + transparentIndex: transparentIndex, + dispose: 1 + }); + + p.html( + 'Rendered frame ' + i.toString() + ' out of ' + nFrames.toString() + ); + + // this just makes the process asynchronous, preventing + // that the encoding locks up the browser + await new Promise(resolve => setTimeout(resolve, 0)); + } + + gif.finish(); + + // Get a direct typed array view into the buffer to avoid copying it + const buffer = gif.bytesView(); + const extension = 'gif'; + + const blob = new Blob([buffer], { + type: 'image/gif' + }); + + frames = []; + this.loop(); + + p.html('Done. Downloading your gif!🌸'); + p5.prototype.downloadFile(blob, fileName, extension); +}; + +function _flipPixels(pixels) { + // extracting the pixels using readPixels returns + // an upside down image. we have to flip it back + // first. this solution is proposed by gman on + // this stack overflow answer: + // https://stackoverflow.com/questions/41969562/how-can-i-flip-the-result-of-webglrenderingcontext-readpixels + + var halfHeight = parseInt(height / 2); + var bytesPerRow = width * 4; + + // make a temp buffer to hold one row + var temp = new Uint8Array(width * 4); + for (var y = 0; y < halfHeight; ++y) { + var topOffset = y * bytesPerRow; + var bottomOffset = (height - y - 1) * bytesPerRow; + + // make copy of a row on the top half + temp.set(pixels.subarray(topOffset, topOffset + bytesPerRow)); + + // copy a row from the bottom half to the top + pixels.copyWithin(topOffset, bottomOffset, bottomOffset + bytesPerRow); + + // copy the copy of the top half row to the bottom half + pixels.set(temp, bottomOffset); + } + return pixels; +} + +function _generateGlobalPalette(frames) { + // make an array the size of every possible color in every possible frame + // that is: width * height * frames. + let allColors = new Uint8Array(frames.length * frames[0].length); + + // put every frame one after the other in sequence. + // this array will hold absolutely every pixel from the animation. + // the set function on the Uint8Array works super fast tho! + for (let f = 0; f < frames.length; f++) { + allColors.set(frames[0], f * frames[0].length); + } + + // quantize this massive array into 256 colors and return it! + let colorPalette = quantize(allColors, 256, { + format: 'rgba444', + oneBitAlpha: true + }); + + // when generating the palette, we have to leave space for 1 of the + // indices to be a random color that does not appear anywhere in our + // animation to use for transparency purposes. So, if the palette is full + // (has 256 colors), we overwrite the last one with a random, fully transparent + // color. Otherwise, we just push a new color into the palette the same way. + + // this guarantees that when using the transparency index, there are no matches + // between some colors of the animation and the "holes" we want to dig on them, + // which would cause pieces of some frames to be transparent and thus look glitchy. + if (colorPalette.length === 256) { + colorPalette[colorPalette.length - 1] = [ + Math.random() * 255, + Math.random() * 255, + Math.random() * 255, + 0 + ]; + } else { + colorPalette.push([ + Math.random() * 255, + Math.random() * 255, + Math.random() * 255, + 0 + ]); + } + return colorPalette; +} + +function _pixelEquals(a, b) { + return ( + Array.isArray(a) && + Array.isArray(b) && + a.length === b.length && + a.every((val, index) => val === b[index]) + ); +} + /** * Helper function for loading GIF-based images */ @@ -602,33 +987,8 @@ p5.prototype.noTint = function() { * @param {p5.Image} The image to be tinted * @return {canvas} The resulting tinted canvas */ -p5.prototype._getTintedImageCanvas = function(img) { - if (!img.canvas) { - return img; - } - const pixels = Filters._toPixels(img.canvas); - const tmpCanvas = document.createElement('canvas'); - tmpCanvas.width = img.canvas.width; - tmpCanvas.height = img.canvas.height; - const tmpCtx = tmpCanvas.getContext('2d'); - const id = tmpCtx.createImageData(img.canvas.width, img.canvas.height); - const newPixels = id.data; - - for (let i = 0; i < pixels.length; i += 4) { - const r = pixels[i]; - const g = pixels[i + 1]; - const b = pixels[i + 2]; - const a = pixels[i + 3]; - - newPixels[i] = r * this._renderer._tint[0] / 255; - newPixels[i + 1] = g * this._renderer._tint[1] / 255; - newPixels[i + 2] = b * this._renderer._tint[2] / 255; - newPixels[i + 3] = a * this._renderer._tint[3] / 255; - } - - tmpCtx.putImageData(id, 0, 0); - return tmpCanvas; -}; +p5.prototype._getTintedImageCanvas = + p5.Renderer2D.prototype._getTintedImageCanvas; /** * Set image mode. Modifies the location from which images are drawn by diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 97253ec1ad..87cd1b0954 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -629,10 +629,6 @@ p5.Image.prototype.copy = function(...args) { * http://blogs.adobe.com/webplatform/2013/01/28/blending-features-in-canvas/ */ // TODO: - Accept an array of alpha values. -// - Use other channels of an image. p5 uses the -// blue channel (which feels kind of arbitrary). Note: at the -// moment this method does not match native processing's original -// functionality exactly. p5.Image.prototype.mask = function(p5Image) { if (p5Image === undefined) { p5Image = this; @@ -657,7 +653,29 @@ p5.Image.prototype.mask = function(p5Image) { ]; this.drawingContext.globalCompositeOperation = 'destination-in'; - p5.Image.prototype.copy.apply(this, copyArgs); + if (this.gifProperties) { + for (let i = 0; i < this.gifProperties.frames.length; i++) { + this.drawingContext.putImageData( + this.gifProperties.frames[i].image, + 0, + 0 + ); + p5.Image.prototype.copy.apply(this, copyArgs); + this.gifProperties.frames[i].image = this.drawingContext.getImageData( + 0, + 0, + this.width, + this.height + ); + } + this.drawingContext.putImageData( + this.gifProperties.frames[this.gifProperties.displayIndex].image, + 0, + 0 + ); + } else { + p5.Image.prototype.copy.apply(this, copyArgs); + } this.drawingContext.globalCompositeOperation = currBlend; this.setModified(true); }; @@ -890,7 +908,7 @@ p5.Image.prototype.isModified = function() { */ p5.Image.prototype.save = function(filename, extension) { if (this.gifProperties) { - p5.prototype.saveGif(this, filename); + p5.prototype.encodeAndDownloadGif(this, filename); } else { p5.prototype.saveCanvas(this.canvas, filename, extension); } diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index d7ba19b7e5..9ca2da949a 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1490,6 +1490,7 @@ p5.Vector.prototype.heading = function heading() { */ p5.Vector.prototype.setHeading = function setHeading(a) { + if (this.isPInst) a = this._toRadians(a); let m = this.mag(); this.x = m * Math.cos(a); this.y = m * Math.sin(a); diff --git a/src/math/trigonometry.js b/src/math/trigonometry.js index dfbbfc1cb0..536d1d575d 100644 --- a/src/math/trigonometry.js +++ b/src/math/trigonometry.js @@ -281,9 +281,9 @@ p5.prototype.radians = angle => angle * constants.DEG_TO_RAD; /** * Sets the current mode of p5 to the given mode. Default mode is RADIANS. * + * Calling angleMode() with no arguments returns current anglemode. * @method angleMode * @param {Constant} mode either RADIANS or DEGREES - * * @example *
    * @@ -306,8 +306,15 @@ p5.prototype.radians = angle => angle * constants.DEG_TO_RAD; *
    * */ +/** + * @method angleMode + * @return {Constant} mode either RADIANS or DEGREES + */ p5.prototype.angleMode = function(mode) { - if (mode === constants.DEGREES || mode === constants.RADIANS) { + p5._validateParameters('angleMode', arguments); + if (typeof mode === 'undefined') { + return this._angleMode; + } else if (mode === constants.DEGREES || mode === constants.RADIANS) { this._angleMode = mode; } }; diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 090a89479e..9e258c2bbe 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -27,6 +27,7 @@ import * as constants from '../core/constants'; * // with width 50 and height 50 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a white plane with black wireframe lines'); * } * * function draw() { @@ -98,7 +99,7 @@ p5.prototype.plane = function(width, height, detailX, detailY) { * Draw a box with given width, height and depth * @method box * @param {Number} [width] width of the box - * @param {Number} [Height] height of the box + * @param {Number} [height] height of the box * @param {Number} [depth] depth of the box * @param {Integer} [detailX] Optional number of triangle * subdivisions in x-dimension @@ -112,6 +113,7 @@ p5.prototype.plane = function(width, height, detailX, detailY) { * // with width, height and depth of 50 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a white box rotating in 3D space'); * } * * function draw() { @@ -231,6 +233,7 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * // draw a sphere with radius 40 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a white sphere with black wireframe lines'); * } * * function draw() { @@ -250,6 +253,9 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * detailX = createSlider(3, 24, 3); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a white sphere with low detail on the x-axis, including a slider to adjust detailX' + * ); * } * * function draw() { @@ -270,6 +276,9 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { * detailY = createSlider(3, 16, 3); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a white sphere with low detail on the y-axis, including a slider to adjust detailY' + * ); * } * * function draw() { @@ -441,6 +450,7 @@ const _truncatedCone = function( * // with radius 20 and height 50 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a rotating white cylinder'); * } * * function draw() { @@ -462,6 +472,9 @@ const _truncatedCone = function( * detailX = createSlider(3, 24, 3); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a rotating white cylinder with limited X detail, with a slider that adjusts detailX' + * ); * } * * function draw() { @@ -482,6 +495,9 @@ const _truncatedCone = function( * detailY = createSlider(1, 16, 1); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a rotating white cylinder with limited Y detail, with a slider that adjusts detailY' + * ); * } * * function draw() { @@ -576,6 +592,7 @@ p5.prototype.cylinder = function( * // with radius 40 and height 70 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a rotating white cone'); * } * * function draw() { @@ -597,6 +614,9 @@ p5.prototype.cylinder = function( * detailX = createSlider(3, 16, 3); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a rotating white cone with limited X detail, with a slider that adjusts detailX' + * ); * } * * function draw() { @@ -617,6 +637,9 @@ p5.prototype.cylinder = function( * detailY = createSlider(3, 16, 3); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a rotating white cone with limited Y detail, with a slider that adjusts detailY' + * ); * } * * function draw() { @@ -692,6 +715,7 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * // with radius 30, 40 and 40. * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a white 3d ellipsoid'); * } * * function draw() { @@ -711,6 +735,9 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * detailX = createSlider(2, 24, 12); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a rotating white ellipsoid with limited X detail, with a slider that adjusts detailX' + * ); * } * * function draw() { @@ -731,6 +758,9 @@ p5.prototype.cone = function(radius, height, detailX, detailY, cap) { * detailY = createSlider(2, 24, 6); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a rotating white ellipsoid with limited Y detail, with a slider that adjusts detailY' + * ); * } * * function draw() { @@ -826,6 +856,7 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * // with ring radius 30 and tube radius 15 * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a rotating white torus'); * } * * function draw() { @@ -847,6 +878,9 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * detailX = createSlider(3, 24, 3); * detailX.position(10, height + 5); * detailX.style('width', '80px'); + * describe( + * 'a rotating white torus with limited X detail, with a slider that adjusts detailX' + * ); * } * * function draw() { @@ -867,6 +901,9 @@ p5.prototype.ellipsoid = function(radiusX, radiusY, radiusZ, detailX, detailY) { * detailY = createSlider(3, 16, 3); * detailY.position(10, height + 5); * detailY.style('width', '80px'); + * describe( + * 'a rotating white torus with limited Y detail, with a slider that adjusts detailY' + * ); * } * * function draw() { @@ -1180,56 +1217,134 @@ p5.RendererGL.prototype.arc = function(args) { }; p5.RendererGL.prototype.rect = function(args) { - const perPixelLighting = this._pInst._glAttributes.perPixelLighting; const x = args[0]; const y = args[1]; const width = args[2]; const height = args[3]; - const detailX = args[4] || (perPixelLighting ? 1 : 24); - const detailY = args[5] || (perPixelLighting ? 1 : 16); - const gId = `rect|${detailX}|${detailY}`; - if (!this.geometryInHash(gId)) { - const _rect = function() { - for (let i = 0; i <= this.detailY; i++) { - const v = i / this.detailY; - for (let j = 0; j <= this.detailX; j++) { - const u = j / this.detailX; - const p = new p5.Vector(u, v, 0); - this.vertices.push(p); - this.uvs.push(u, v); + + if (typeof args[4] === 'undefined') { + // Use the retained mode for drawing rectangle, + // if args for rounding rectangle is not provided by user. + const perPixelLighting = this._pInst._glAttributes.perPixelLighting; + const detailX = args[4] || (perPixelLighting ? 1 : 24); + const detailY = args[5] || (perPixelLighting ? 1 : 16); + const gId = `rect|${detailX}|${detailY}`; + if (!this.geometryInHash(gId)) { + const _rect = function() { + for (let i = 0; i <= this.detailY; i++) { + const v = i / this.detailY; + for (let j = 0; j <= this.detailX; j++) { + const u = j / this.detailX; + const p = new p5.Vector(u, v, 0); + this.vertices.push(p); + this.uvs.push(u, v); + } } - } - // using stroke indices to avoid stroke over face(s) of rectangle - if (detailX > 0 && detailY > 0) { - this.strokeIndices = [ - [0, detailX], - [detailX, (detailX + 1) * (detailY + 1) - 1], - [(detailX + 1) * (detailY + 1) - 1, (detailX + 1) * detailY], - [(detailX + 1) * detailY, 0] - ]; - } - }; - const rectGeom = new p5.Geometry(detailX, detailY, _rect); - rectGeom - .computeFaces() - .computeNormals() - ._makeTriangleEdges() - ._edgesToVertices(); - this.createBuffers(gId, rectGeom); - } + // using stroke indices to avoid stroke over face(s) of rectangle + if (detailX > 0 && detailY > 0) { + this.strokeIndices = [ + [0, detailX], + [detailX, (detailX + 1) * (detailY + 1) - 1], + [(detailX + 1) * (detailY + 1) - 1, (detailX + 1) * detailY], + [(detailX + 1) * detailY, 0] + ]; + } + }; + const rectGeom = new p5.Geometry(detailX, detailY, _rect); + rectGeom + .computeFaces() + .computeNormals() + ._makeTriangleEdges() + ._edgesToVertices(); + this.createBuffers(gId, rectGeom); + } - // only a single rectangle (of a given detail) is cached: a square with - // opposite corners at (0,0) & (1,1). - // - // before rendering, this square is scaled & moved to the required location. - const uMVMatrix = this.uMVMatrix.copy(); - try { - this.uMVMatrix.translate([x, y, 0]); - this.uMVMatrix.scale(width, height, 1); + // only a single rectangle (of a given detail) is cached: a square with + // opposite corners at (0,0) & (1,1). + // + // before rendering, this square is scaled & moved to the required location. + const uMVMatrix = this.uMVMatrix.copy(); + try { + this.uMVMatrix.translate([x, y, 0]); + this.uMVMatrix.scale(width, height, 1); + + this.drawBuffers(gId); + } finally { + this.uMVMatrix = uMVMatrix; + } + } else { + // Use Immediate mode to round the rectangle corner, + // if args for rounding corners is provided by user + let tl = args[4]; + let tr = typeof args[5] === 'undefined' ? tl : args[5]; + let br = typeof args[6] === 'undefined' ? tr : args[6]; + let bl = typeof args[7] === 'undefined' ? br : args[7]; + + let a = x; + let b = y; + let c = width; + let d = height; + + c += a; + d += b; + + if (a > c) { + const temp = a; + a = c; + c = temp; + } - this.drawBuffers(gId); - } finally { - this.uMVMatrix = uMVMatrix; + if (b > d) { + const temp = b; + b = d; + d = temp; + } + + const maxRounding = Math.min((c - a) / 2, (d - b) / 2); + if (tl > maxRounding) tl = maxRounding; + if (tr > maxRounding) tr = maxRounding; + if (br > maxRounding) br = maxRounding; + if (bl > maxRounding) bl = maxRounding; + + let x1 = a; + let y1 = b; + let x2 = c; + let y2 = d; + + this.beginShape(); + if (tr !== 0) { + this.vertex(x2 - tr, y1); + this.quadraticVertex(x2, y1, x2, y1 + tr); + } else { + this.vertex(x2, y1); + } + if (br !== 0) { + this.vertex(x2, y2 - br); + this.quadraticVertex(x2, y2, x2 - br, y2); + } else { + this.vertex(x2, y2); + } + if (bl !== 0) { + this.vertex(x1 + bl, y2); + this.quadraticVertex(x1, y2, x1, y2 - bl); + } else { + this.vertex(x1, y2); + } + if (tl !== 0) { + this.vertex(x1, y1 + tl); + this.quadraticVertex(x1, y1, x1 + tl, y1); + } else { + this.vertex(x1, y1); + } + + this.immediateMode.geometry.uvs.length = 0; + for (const vert of this.immediateMode.geometry.vertices) { + const u = (vert.x - x1) / width; + const v = (vert.y - y1) / height; + this.immediateMode.geometry.uvs.push(u, v); + } + + this.endShape(constants.CLOSE); } return this; }; diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js index 87680921df..d13a9acd3e 100644 --- a/src/webgl/interaction.js +++ b/src/webgl/interaction.js @@ -30,6 +30,9 @@ import * as constants from '../core/constants'; * function setup() { * createCanvas(100, 100, WEBGL); * normalMaterial(); + * describe( + * 'Camera orbits around a box when mouse is hold-clicked & then moved.' + * ); * } * function draw() { * background(200); @@ -167,6 +170,9 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(); + * describe( + * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z. the grid and icon disappear when the spacebar is pressed.' + * ); * } * * function draw() { @@ -194,6 +200,7 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(GRID); + * describe('a 3D box is centered on a grid in a 3D sketch.'); * } * * function draw() { @@ -214,6 +221,9 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(AXES); + * describe( + * 'a 3D box is centered in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z.' + * ); * } * * function draw() { @@ -236,6 +246,7 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(GRID, 100, 10, 0, 0, 0); + * describe('a 3D box is centered on a grid in a 3D sketch'); * } * * function draw() { @@ -256,6 +267,9 @@ p5.prototype.orbitControl = function(sensitivityX, sensitivityY, sensitivityZ) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(100, 10, 0, 0, 0, 20, 0, -40, 0); + * describe( + * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z.' + * ); * } * * function draw() { @@ -361,6 +375,9 @@ p5.prototype.debugMode = function(...args) { * camera(0, -30, 100, 0, 0, 0, 0, 1, 0); * normalMaterial(); * debugMode(); + * describe( + * 'a 3D box is centered on a grid in a 3D sketch. an icon indicates the direction of each axis: a red line points +X, a green line +Y, and a blue line +Z. the grid and icon disappear when the spacebar is pressed.' + * ); * } * * function draw() { diff --git a/src/webgl/light.js b/src/webgl/light.js index 559f3a468e..c5df0add0a 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -37,6 +37,7 @@ import * as constants from '../core/constants'; * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe('sphere with coral color under black light'); * } * function draw() { * background(100); @@ -55,6 +56,7 @@ import * as constants from '../core/constants'; * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe('sphere with coral color under white light'); * } * function draw() { * background(100); @@ -144,6 +146,9 @@ p5.prototype.ambientLight = function(v1, v2, v3, a) { * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe( + * 'Sphere with specular highlight. Clicking the mouse toggles the specular highlight color between red and the default white.' + * ); * } * * function draw() { @@ -256,6 +261,9 @@ p5.prototype.specularColor = function(v1, v2, v3) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe( + * 'scene with sphere and directional light. The direction of the light is controlled with the mouse position.' + * ); * } * function draw() { * background(0); @@ -374,6 +382,9 @@ p5.prototype.directionalLight = function(v1, v2, v3, x, y, z) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe( + * 'scene with sphere and point light. The position of the light is controlled with the mouse position.' + * ); * } * function draw() { * background(0); @@ -481,6 +492,7 @@ p5.prototype.pointLight = function(v1, v2, v3, x, y, z) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('the light is partially ambient and partially directional'); * } * function draw() { * background(0); @@ -534,6 +546,9 @@ p5.prototype.lights = function() { * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe( + * 'Two spheres with different falloff values show different intensity of light' + * ); * } * function draw() { * ortho(); @@ -654,6 +669,9 @@ p5.prototype.lightFalloff = function( * * function setup() { * createCanvas(100, 100, WEBGL); + * describe( + * 'scene with sphere and spot light. The position of the light is controlled with the mouse position.' + * ); * } * function draw() { * background(0); @@ -985,6 +1003,9 @@ p5.prototype.spotLight = function( * * function setup() { * createCanvas(100, 100, WEBGL); + * describe( + * 'Three white spheres. Each appears as a different color due to lighting.' + * ); * } * function draw() { * background(200); diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 78e19145c2..039eba7fea 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -50,6 +50,7 @@ import './p5.Geometry'; * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('Vertically rotating 3-d octahedron.'); * } * * function draw() { @@ -77,6 +78,7 @@ import './p5.Geometry'; * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('Vertically rotating 3-d teapot with red, green and blue gradient.'); * } * * function draw() { @@ -602,6 +604,7 @@ function parseASCIISTL(model, lines) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('Vertically rotating 3-d octahedron.'); * } * * function draw() { diff --git a/src/webgl/material.js b/src/webgl/material.js index 08b9f7ddd5..a42f569bf6 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -45,6 +45,7 @@ import './p5.Texture'; * shader(mandel); * noStroke(); * mandel.setUniform('p', [-0.74364388703, 0.13182590421]); + * describe('zooming Mandelbrot set. a colorful, infinitely detailed fractal.'); * } * * function draw() { @@ -162,6 +163,7 @@ p5.prototype.loadShader = function( * * // 'p' is the center point of the Mandelbrot image * mandel.setUniform('p', [-0.74364388703, 0.13182590421]); + * describe('zooming Mandelbrot set. a colorful, infinitely detailed fractal.'); * } * * function draw() { @@ -233,6 +235,10 @@ p5.prototype.createShader = function(vertSrc, fragSrc) { * orangeBlue.setUniform('colorBackground', [0.226, 0.0, 0.615]); * * noStroke(); + * + * describe( + * 'canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed.' + * ); * } * * function draw() { @@ -326,6 +332,10 @@ p5.prototype.shader = function(s) { * * // Create our shader * shaderProgram = createShader(vertSrc, fragSrc); + * + * describe( + * 'Two rotating cubes. The left one is painted using a custom (user-defined) shader, while the right one is painted using the default fill shader.' + * ); * } * * // prettier-ignore @@ -392,6 +402,7 @@ p5.prototype.resetShader = function() { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('spinning cube with a texture from an image'); * } * * function draw() { @@ -417,6 +428,7 @@ p5.prototype.resetShader = function() { * createCanvas(100, 100, WEBGL); * pg = createGraphics(200, 200); * pg.textSize(75); + * describe('plane with a texture from an image created by createGraphics()'); * } * * function draw() { @@ -444,6 +456,7 @@ p5.prototype.resetShader = function() { * } * function setup() { * createCanvas(100, 100, WEBGL); + * describe('rectangle with video as texture'); * } * * function draw() { @@ -473,6 +486,7 @@ p5.prototype.resetShader = function() { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('quad with a texture, mapped using normalized coordinates'); * } * * function draw() { @@ -529,6 +543,7 @@ p5.prototype.texture = function(tex) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('quad with a texture, mapped using normalized coordinates'); * } * * function draw() { @@ -557,6 +572,7 @@ p5.prototype.texture = function(tex) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('quad with a texture, mapped using image coordinates'); * } * * function draw() { @@ -615,6 +631,7 @@ p5.prototype.textureMode = function(mode) { * function setup() { * createCanvas(100, 100, WEBGL); * textureWrap(MIRROR); + * describe('an image of the rocky mountains repeated in mirrored tiles'); * } * * function draw() { @@ -675,6 +692,7 @@ p5.prototype.textureWrap = function(wrapX, wrapY = wrapX) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('Sphere with normal material'); * } * * function draw() { @@ -731,6 +749,7 @@ p5.prototype.normalMaterial = function(...args) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('sphere reflecting red, blue, and green light'); * } * function draw() { * background(0); @@ -751,6 +770,7 @@ p5.prototype.normalMaterial = function(...args) { * // so object only reflects it's red and blue components * function setup() { * createCanvas(100, 100, WEBGL); + * describe('box reflecting only red and blue light'); * } * function draw() { * background(70); @@ -770,6 +790,7 @@ p5.prototype.normalMaterial = function(...args) { * // green, it does not reflect any light * function setup() { * createCanvas(100, 100, WEBGL); + * describe('box reflecting no light'); * } * function draw() { * background(70); @@ -839,6 +860,7 @@ p5.prototype.ambientMaterial = function(v1, v2, v3) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('sphere with green emissive material'); * } * function draw() { * background(0); @@ -913,6 +935,7 @@ p5.prototype.emissiveMaterial = function(v1, v2, v3, a) { * function setup() { * createCanvas(100, 100, WEBGL); * noStroke(); + * describe('torus with specular material'); * } * * function draw() { @@ -982,6 +1005,7 @@ p5.prototype.specularMaterial = function(v1, v2, v3, alpha) { * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('two spheres, one more shiny than the other'); * } * function draw() { * background(0); @@ -1026,7 +1050,10 @@ p5.RendererGL.prototype._applyColorBlend = function(colors) { const isTexture = this.drawMode === constants.TEXTURE; const doBlend = - isTexture || colors[colors.length - 1] < 1.0 || this._isErasing; + isTexture || + this.curBlendMode !== constants.BLEND || + colors[colors.length - 1] < 1.0 || + this._isErasing; if (doBlend !== this._isBlending) { if ( @@ -1057,10 +1084,13 @@ p5.RendererGL.prototype._applyBlendMode = function() { const gl = this.GL; switch (this.curBlendMode) { case constants.BLEND: - case constants.ADD: gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); break; + case constants.ADD: + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE); + break; case constants.REMOVE: gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT); gl.blendFunc(gl.SRC_ALPHA, gl.DST_ALPHA); diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index f09dee3923..637ada8e69 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -45,6 +45,7 @@ import p5 from '../core/main'; * * function setup() { * createCanvas(100, 100, WEBGL); + * describe('a square moving closer and then away from the camera.'); * } * function draw() { * background(204); @@ -82,6 +83,9 @@ import p5 from '../core/main'; * sliderGroup[i].position(10, height + h); * sliderGroup[i].style('width', '80px'); * } + * describe( + * 'White square repeatedly grows to fill canvas and then shrinks. An interactive example of a red cube with 3 sliders for moving it across x, y, z axis and 3 sliders for shifting its center.' + * ); * } * * function draw() { @@ -141,6 +145,9 @@ p5.prototype.camera = function(...args) { * function setup() { * createCanvas(100, 100, WEBGL); * perspective(PI / 3.0, width / height, 0.1, 500); + * describe( + * 'two colored 3D boxes move back and forth, rotating as mouse is dragged.' + * ); * } * function draw() { * background(200); @@ -203,6 +210,9 @@ p5.prototype.perspective = function(...args) { * function setup() { * createCanvas(100, 100, WEBGL); * ortho(-width / 2, width / 2, height / 2, -height / 2, 0, 500); + * describe( + * 'two 3D boxes move back and forth along same plane, rotating as mouse is dragged.' + * ); * } * function draw() { * background(200); @@ -266,6 +276,9 @@ p5.prototype.ortho = function(...args) { * createCanvas(100, 100, WEBGL); * setAttributes('antialias', true); * frustum(-0.1, 0.1, -0.1, 0.1, 0.1, 200); + * describe( + * 'two 3D boxes move back and forth along same plane, rotating as mouse is dragged.' + * ); * } * function draw() { * background(200); @@ -328,6 +341,7 @@ p5.prototype.frustum = function(...args) { * createCanvas(100, 100, WEBGL); * background(0); * camera = createCamera(); + * describe('An example that creates a camera and moves it around the box.'); * } * * function draw() { @@ -398,6 +412,9 @@ p5.prototype.createCamera = function() { * cam = createCamera(); * // set initial pan angle * cam.pan(-0.8); + * describe( + * 'camera view pans left and right across a series of rotating 3D boxes.' + * ); * } * * function draw() { @@ -455,6 +472,7 @@ p5.Camera = function(renderer) { * cam = createCamera(); * div = createDiv(); * div.position(0, 0); + * describe('An example showing the use of camera object properties'); * } * * function draw() { @@ -482,6 +500,7 @@ p5.Camera = function(renderer) { * cam = createCamera(); * div = createDiv(); * div.position(0, 0); + * describe('An example showing the use of camera object properties'); * } * * function draw() { @@ -509,6 +528,7 @@ p5.Camera = function(renderer) { * cam = createCamera(); * div = createDiv(); * div.position(0, 0); + * describe('An example showing the use of camera object properties'); * } * * function draw() { @@ -538,6 +558,7 @@ p5.Camera = function(renderer) { * div = createDiv('centerX = ' + cam.centerX); * div.position(0, 0); * div.style('color', 'white'); + * describe('An example showing the use of camera object properties'); * } * * function draw() { @@ -566,6 +587,7 @@ p5.Camera = function(renderer) { * div = createDiv('centerY = ' + cam.centerY); * div.position(0, 0); * div.style('color', 'white'); + * describe('An example showing the use of camera object properties'); * } * * function draw() { @@ -594,6 +616,7 @@ p5.Camera = function(renderer) { * div = createDiv('centerZ = ' + cam.centerZ); * div.position(0, 0); * div.style('color', 'white'); + * describe('An example showing the use of camera object properties'); * } * * function draw() { @@ -622,6 +645,7 @@ p5.Camera = function(renderer) { * div.position(0, 0); * div.style('color', 'blue'); * div.style('font-size', '18px'); + * describe('An example showing the use of camera object properties'); * } *
    * @@ -645,6 +669,7 @@ p5.Camera = function(renderer) { * div.position(0, 0); * div.style('color', 'blue'); * div.style('font-size', '18px'); + * describe('An example showing the use of camera object properties'); * } * * @@ -668,6 +693,7 @@ p5.Camera = function(renderer) { * div.position(0, 0); * div.style('color', 'blue'); * div.style('font-size', '18px'); + * describe('An example showing the use of camera object properties'); * } * * @@ -1744,6 +1770,10 @@ p5.Camera.prototype._isActive = function() { * * // set variable for previously active camera: * currentCamera = 1; + * + * describe( + * 'Canvas switches between two camera views, each showing a series of spinning 3D boxes.' + * ); * } * * function draw() { diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 3c23b0c56e..2bfc694ba8 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -26,7 +26,8 @@ import './p5.RenderBuffer'; * @param {Number} mode webgl primitives mode. beginShape supports the * following modes: * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) + * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, + * and TESS(WEBGL only) * @chainable */ p5.RendererGL.prototype.beginShape = function(mode) { @@ -36,6 +37,13 @@ p5.RendererGL.prototype.beginShape = function(mode) { return this; }; +const immediateBufferStrides = { + vertices: 1, + vertexNormals: 1, + vertexColors: 4, + uvs: 2 +}; + /** * adds a vertex to be drawn in a custom Shape. * @private @@ -47,6 +55,32 @@ p5.RendererGL.prototype.beginShape = function(mode) { * @TODO implement handling of p5.Vector args */ p5.RendererGL.prototype.vertex = function(x, y) { + // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn + // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra + // work to convert QUAD_STRIP here, since the only difference is in how edges + // are rendered.) + if (this.immediateMode.shapeMode === constants.QUADS) { + // A finished quad turned into triangles should leave 6 vertices in the + // buffer: + // 0--3 0 3--5 + // | | --> | \ \ | + // 1--2 1--2 4 + // When vertex index 3 is being added, add the necessary duplicates. + if (this.immediateMode.geometry.vertices.length % 6 === 3) { + for (const key in immediateBufferStrides) { + const stride = immediateBufferStrides[key]; + const buffer = this.immediateMode.geometry[key]; + buffer.push( + ...buffer.slice( + buffer.length - 3 * stride, + buffer.length - 2 * stride + ), + ...buffer.slice(buffer.length - stride, buffer.length) + ); + } + } + } + let z, u, v; // default to (x, y) mode: all other arguments assumed to be 0. @@ -233,6 +267,29 @@ p5.RendererGL.prototype._calculateEdges = function( res.push([i, i + 1]); } break; + case constants.QUADS: + // Quads have been broken up into two triangles by `vertex()`: + // 0 3--5 + // | \ \ | + // 1--2 4 + for (i = 0; i < verts.length - 5; i += 6) { + res.push([i, i + 1]); + res.push([i + 1, i + 2]); + res.push([i + 3, i + 5]); + res.push([i + 4, i + 5]); + } + break; + case constants.QUAD_STRIP: + // 0---2---4 + // | | | + // 1---3---5 + for (i = 0; i < verts.length - 2; i += 2) { + res.push([i, i + 1]); + res.push([i, i + 2]); + res.push([i + 1, i + 3]); + } + res.push([i, i + 1]); + break; default: for (i = 0; i < verts.length - 1; i++) { res.push([i, i + 1]); @@ -289,6 +346,15 @@ p5.RendererGL.prototype._drawImmediateFill = function() { this.immediateMode.shapeMode = constants.TRIANGLE_FAN; } + // WebGL 1 doesn't support the QUADS and QUAD_STRIP modes, so we + // need to convert them to a supported format. In `vertex()`, we reformat + // the input data into the formats specified below. + if (this.immediateMode.shapeMode === constants.QUADS) { + this.immediateMode.shapeMode = constants.TRIANGLES; + } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) { + this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; + } + this._applyColorBlend(this.curFillColor); gl.drawArrays( this.immediateMode.shapeMode, diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 8e849a830c..6de21ca9b5 100755 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1038,6 +1038,7 @@ p5.RendererGL.prototype.push = function() { properties.drawMode = this.drawMode; properties._currentNormal = this._currentNormal; + properties.curBlendMode = this.curBlendMode; return style; }; diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 740e95978e..5435814f2d 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -341,6 +341,10 @@ p5.Shader.prototype.useProgram = function() { * createCanvas(100, 100, WEBGL); * shader(grad); * noStroke(); + * + * describe( + * 'canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed.' + * ); * } * * function draw() { diff --git a/test/manual-test-examples/p5.Font/textInRect/index.html b/test/manual-test-examples/p5.Font/textInRect/index.html new file mode 100644 index 0000000000..35938afb91 --- /dev/null +++ b/test/manual-test-examples/p5.Font/textInRect/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/manual-test-examples/p5.Font/textInRect/sketch.js b/test/manual-test-examples/p5.Font/textInRect/sketch.js new file mode 100644 index 0000000000..d7d5fa0f49 --- /dev/null +++ b/test/manual-test-examples/p5.Font/textInRect/sketch.js @@ -0,0 +1,81 @@ +let xpos = 50; +let ypos = 100; +let str = + 'One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve Thirteen Fourteen Fifteen Sixteen Seventeen Eighteen Nineteen Twenty Twenty-one Twenty-two Twenty-three Twenty-four Twenty-five Twenty-six Twenty-seven Twenty-eight Twenty-nine Thirty Thirty-one Thirty-two Thirty-three Thirty-four Thirty-five Thirty-six Thirty-seven Thirty-eight Thirty-nine Forty Forty-one Forty-two Forty-three Forty-four Forty-five Forty-six Forty-seven Forty-eight Forty-nine Fifty Fifty-one Fifty-two Fifty-three'; + +function setup() { + createCanvas(1050, 800); + background(245); + + let ta = textAscent(); + + textAlign(CENTER, TOP); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200, 200); + xpos += 250; + + textAlign(CENTER, CENTER); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200, 200); + xpos += 250; + + textAlign(CENTER, BOTTOM); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200, 200); + xpos += 250; + + textAlign(CENTER, BASELINE); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200, 200); + + textSize(18); + textAlign(CENTER, TOP); + text('TOP', 150, height / 2 - 40); + text('CENTER', 400, height / 2 - 40); + text('BOTTOM', 650, height / 2 - 40); + text('BASELINE', 900, height / 2 - 40); + textSize(12); + + xpos = 50; + ypos += 400; + + textAlign(CENTER, TOP); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200); + xpos += 250; + + textAlign(CENTER, CENTER); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200); + xpos += 250; + + textAlign(CENTER, BOTTOM); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200); + xpos += 250; + + textAlign(CENTER, BASELINE); + rect(xpos, ypos, 200, 200); + text(str, xpos, ypos, 200); + + textSize(18); + textAlign(CENTER, TOP); + text('TOP', 150, height / 2 - 40); + text('CENTER', 400, height / 2 - 40); + text('BOTTOM', 650, height / 2 - 40); + text('BASELINE', 900, height / 2 - 40); + text('TOP', 150, ypos + 270); + text('CENTER', 400, ypos + 270); + text('BOTTOM', 650, ypos + 270); + text('BASELINE', 900, ypos + 270); + + fill(255); + noStroke(); + textSize(24); + + rect(0, height / 2, width, 15); + fill(0); + textAlign(LEFT, TOP); + text('text(s, x, y, w, h)', 20, 40); + text('text(s, x, y, w) [no height]', 20, height / 2 + 40); +} diff --git a/test/manual-test-examples/tint-performance/flowers-large.jpg b/test/manual-test-examples/tint-performance/flowers-large.jpg new file mode 100644 index 0000000000..1a54909ceb Binary files /dev/null and b/test/manual-test-examples/tint-performance/flowers-large.jpg differ diff --git a/test/manual-test-examples/tint-performance/index.html b/test/manual-test-examples/tint-performance/index.html new file mode 100644 index 0000000000..8624ec9ae5 --- /dev/null +++ b/test/manual-test-examples/tint-performance/index.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/tint-performance/sketch.js b/test/manual-test-examples/tint-performance/sketch.js new file mode 100644 index 0000000000..34666a31fb --- /dev/null +++ b/test/manual-test-examples/tint-performance/sketch.js @@ -0,0 +1,54 @@ +var img; +var times = []; + +function preload() { + img = loadImage('flowers-large.jpg'); +} + +function setup() { + createCanvas(800, 160); +} + +function drawScaledImage(img, x, y) { + push(); + translate(x, y); + scale(0.125); + image(img, 0, 0); + pop(); +} + +function draw() { + times.push(deltaTime); + if (times.length > 60) { + times.shift(); + } + const avgDelta = + times.reduce(function(acc, next) { + return acc + next; + }) / times.length; + const avgRate = 1000 / avgDelta; + + clear(); + push(); + translate(50 * sin(millis() / 1000), 50 * cos(millis() / 1000)); + fill(255, 255, 255); + rect(0, 0, 480, 160); + drawScaledImage(img, 0, 0); + tint(0, 0, 150, 150); // Tint alpha blue + drawScaledImage(img, 160, 0); + tint(255, 255, 255); + drawScaledImage(img, 320, 0); + tint(0, 153, 150); // Tint turquoise + drawScaledImage(img, 480, 0); + noTint(); + drawScaledImage(img, 640, 0); + pop(); + + push(); + textAlign(LEFT, TOP); + textSize(20); + noStroke(); + fill(0); + text(avgRate.toFixed(2) + ' FPS', 10, 10); + pop(); +} diff --git a/test/manual-test-examples/webgl/geometryImmediate/index.html b/test/manual-test-examples/webgl/geometryImmediate/index.html index ddd746b4aa..3c4f912e4c 100644 --- a/test/manual-test-examples/webgl/geometryImmediate/index.html +++ b/test/manual-test-examples/webgl/geometryImmediate/index.html @@ -13,15 +13,17 @@
    - 1: Black Horizontal Line
    + 1: A strip with outlined triangles, three outlines quads, a strip with outlined quads
    + TESTS: beginShape() with TRIANGLE_STRIP, QUADS, and QUAD_STRIP

    + 2: Black Horizontal Line
    TESTS: line()

    - 2: 3 stroked purple shapes with hollow centers
    + 3: 3 stroked purple shapes with hollow centers
    TESTS: tessellation with vertex()

    - 3: 4 squares Red, Blue, Grid, Grid
    + 4: 4 squares Red, Blue, Grid, Grid
    TESTS: vertex with 2, 3, 4, and 5 arguments

    FPS should average higher than 50 FPS on modern laptop/desktop
    - \ No newline at end of file + diff --git a/test/manual-test-examples/webgl/geometryImmediate/sketch.js b/test/manual-test-examples/webgl/geometryImmediate/sketch.js index f6404ea99f..27ce74a562 100644 --- a/test/manual-test-examples/webgl/geometryImmediate/sketch.js +++ b/test/manual-test-examples/webgl/geometryImmediate/sketch.js @@ -1,6 +1,7 @@ let angle, px, py; let img; const sz = 25; +let stripColors = []; function preload() { img = loadImage('../assets/UV_Grid_Sm.jpg'); @@ -12,12 +13,34 @@ function setup() { textureMode(NORMAL); fill(63, 81, 181); strokeWeight(2); + + for (let i = 0; i < 12; i++) { + stripColors.push([random(255), random(255), random(255)]); + } } function draw() { background(250); - line(-width / 2, -180, 0, width / 2, -180, 0); + // Reference: TRIANGLE_STRIP + push(); + translate(-width / 3, -240); + drawStrip(TRIANGLE_STRIP); + pop(); + + // Test 1: QUADS + push(); + translate(0, -240); + drawStrip(QUADS); + pop(); + + // Test 2: QUAD_STRIP + push(); + translate(width / 3, -240); + drawStrip(QUAD_STRIP); + pop(); + + line(-width / 2, -160, 0, width / 2, -160, 0); ngon(5, -200, 0, 120); ngon(8, 0, 0, 120); @@ -26,6 +49,38 @@ function draw() { drawQuads(180); } +function drawStrip(mode) { + rotate(PI / 2); + scale(0.3); + translate(0, -250); + beginShape(mode); + let vertexIndex = 0; + for (let y = 0; y <= 500; y += 100) { + let sides = [-1, 1]; + if (mode === QUADS && y % 200 !== 0) { + // QUAD_STRIP and TRIANGLE_STRIP need the vertices of each shared side + // ordered in the same way: + // 0--2--4--6 + // | | | | ⬇️ + // 1--3--5--7 + // + // ...but QUADS orders vertices in a consisten CCW or CW manner around + // each quad, meaning each side will be in the reverse order of the + // previous: + // 0--3 4--7 + // | | | | πŸ”„ + // 1--2 5--6 + sides.reverse(); + } + for (const side of sides) { + fill(...stripColors[vertexIndex]); + vertex(side * 40, y); + vertexIndex++; + } + } + endShape(); +} + function ngon(n, x, y, d) { beginShape(TESS); for (let i = 0; i < n + 1; i++) { diff --git a/test/manual-test-examples/webgl/stats.js b/test/manual-test-examples/webgl/stats.js index 2f3a23c9f5..1444a81dfa 100644 --- a/test/manual-test-examples/webgl/stats.js +++ b/test/manual-test-examples/webgl/stats.js @@ -10,6 +10,6 @@ requestAnimationFrame(loop); }); }; - script.src = 'http://rawgit.com/mrdoob/stats.js/main/build/stats.min.js'; + script.src = 'http://rawgit.com/mrdoob/stats.js/9d23c79/build/stats.min.js'; document.head.appendChild(script); })(); diff --git a/test/unit/assets/cat-with-hole.png b/test/unit/assets/cat-with-hole.png new file mode 100644 index 0000000000..65b1b87f13 Binary files /dev/null and b/test/unit/assets/cat-with-hole.png differ diff --git a/test/unit/core/error_helpers.js b/test/unit/core/error_helpers.js index 2f5f4b4101..e7a69eb6a2 100644 --- a/test/unit/core/error_helpers.js +++ b/test/unit/core/error_helpers.js @@ -974,4 +974,97 @@ suite('Tests for p5.js sketch_reader', function() { }); } ); + + testUnMinified( + 'detects reassignment of p5.js functions in declaration lists', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + ['function setup() {', 'let x = 2, text = 2;', '}'], + resolve + ); + }).then(function() { + assert.strictEqual(log.length, 1); + assert.match(log[0], /you have used a p5.js reserved function/); + }); + } + ); + + testUnMinified( + 'detects reassignment of p5.js functions in declaration lists after function calls', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + [ + 'function setup() {', + 'let x = constrain(frameCount, 0, 1000), text = 2;', + '}' + ], + resolve + ); + }).then(function() { + assert.strictEqual(log.length, 1); + assert.match(log[0], /you have used a p5.js reserved function/); + }); + } + ); + + testUnMinified( + 'ignores p5.js functions used in the right hand side of assignment expressions', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + // This will still log an error, as `text` isn't being used correctly + // here, but the important part is that it doesn't say that we're + // trying to reassign a reserved function. + ['function draw() {', 'let x = constrain(100, 0, text);', '}'], + resolve + ); + }).then(function() { + assert.ok( + !log.some(line => + line.match(/you have used a p5.js reserved function/) + ) + ); + }); + } + ); + + testUnMinified( + 'ignores p5.js function names used as function arguments', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + ['function draw() {', 'let myLog = (text) => print(text);', '}'], + resolve + ); + }).then(function() { + assert.strictEqual(log.length, 0); + }); + } + ); + + testUnMinified( + 'fails gracefully on inputs too complicated to parse', + function() { + return new Promise(function(resolve) { + prepSketchReaderTest( + // This technically is redefining text, but it should stop parsing + // after the double nested brackets rather than try and possibly + // give a false positive error. This particular assignment will get + // caught at runtime regardless by + // `_createFriendlyGlobalFunctionBinder`. + [ + 'function draw() {', + 'let x = constrain(millis(), 0, text = 100)', + '}' + ], + resolve + ); + }).then(function() { + console.log(log); + assert.strictEqual(log.length, 0); + }); + } + ); }); diff --git a/test/unit/image/downloading.js b/test/unit/image/downloading.js index e4973151e8..fa3df09dd1 100644 --- a/test/unit/image/downloading.js +++ b/test/unit/image/downloading.js @@ -32,16 +32,16 @@ suite('downloading animated gifs', function() { }); }); - suite('p5.prototype.saveGif', function() { + suite('p5.prototype.encodeAndDownloadGif', function() { test('should be a function', function() { - assert.ok(myp5.saveGif); - assert.typeOf(myp5.saveGif, 'function'); + assert.ok(myp5.encodeAndDownloadGif); + assert.typeOf(myp5.encodeAndDownloadGif, 'function'); }); test('should not throw an error', function() { - myp5.saveGif(myGif); + myp5.encodeAndDownloadGif(myGif); }); testWithDownload('should download a gif', function(blobContainer) { - myp5.saveGif(myGif); + myp5.encodeAndDownloadGif(myGif); let gifBlob = blobContainer.blob; assert.strictEqual(gifBlob.type, 'image/gif'); }); @@ -320,3 +320,60 @@ suite('p5.prototype.saveFrames', function() { }); }); }); + +suite('p5.prototype.saveGif', function() { + setup(function(done) { + new p5(function(p) { + p.setup = function() { + myp5 = p; + p.createCanvas(10, 10); + done(); + }; + }); + }); + + teardown(function() { + myp5.remove(); + }); + + test('should be a function', function() { + assert.ok(myp5.saveGif); + assert.typeOf(myp5.saveGif, 'function'); + }); + + test('should not throw an error', function() { + myp5.saveGif('myGif', 3); + }); + + test('should not throw an error', function() { + myp5.saveGif('myGif', 3, { delay: 2, frames: 'seconds' }); + }); + + test('wrong parameter type #0', function(done) { + assert.validationError(function() { + myp5.saveGif(2, 2); + done(); + }); + }); + + test('wrong parameter type #1', function(done) { + assert.validationError(function() { + myp5.saveGif('mySketch', '2'); + done(); + }); + }); + + test('wrong parameter type #2', function(done) { + assert.validationError(function() { + myp5.saveGif('mySketch', 2, 'delay'); + done(); + }); + }); + + testWithDownload('should download a GIF', async function(blobContainer) { + myp5.saveGif(myGif, 3, 2); + await waitForBlob(blobContainer); + let gifBlob = blobContainer.blob; + assert.strictEqual(gifBlob.type, 'image/gif'); + }); +}); diff --git a/test/unit/image/loading.js b/test/unit/image/loading.js index 557de60f29..cad2c2a3e8 100644 --- a/test/unit/image/loading.js +++ b/test/unit/image/loading.js @@ -340,3 +340,106 @@ suite('loading animated gif images', function() { new p5(mySketch, null, false); }); }); + +suite('displaying images', function() { + var myp5; + var pImg; + var imagePath = 'unit/assets/cat-with-hole.png'; + var chanNames = ['red', 'green', 'blue', 'alpha']; + + setup(function(done) { + new p5(function(p) { + p.setup = function() { + myp5 = p; + myp5.pixelDensity(1); + myp5.loadImage( + imagePath, + function(img) { + pImg = img; + myp5.resizeCanvas(pImg.width, pImg.height); + done(); + }, + function() { + throw new Error('Error loading image'); + } + ); + }; + }); + }); + + teardown(function() { + myp5.remove(); + }); + + function checkTint(tintColor) { + myp5.loadPixels(); + pImg.loadPixels(); + for (var i = 0; i < myp5.pixels.length; i += 4) { + var x = (i / 4) % myp5.width; + var y = Math.floor(i / 4 / myp5.width); + for (var chan = 0; chan < tintColor.length; chan++) { + var inAlpha = 1; + var outAlpha = 1; + if (chan < 3) { + // The background of the canvas is black, so after applying the + // image's own alpha + the tint alpha to its color channels, we + // should arrive at the same color that we see on the canvas. + inAlpha = tintColor[3] / 255; + outAlpha = pImg.pixels[i + 3] / 255; + + // Applying the tint involves un-multiplying the alpha of the source + // image, which causes a bit of loss of precision. I'm allowing a + // loss of 10 / 255 in this test. + assert.approximately( + myp5.pixels[i + chan], + pImg.pixels[i + chan] * + (tintColor[chan] / 255) * + outAlpha * + inAlpha, + 10, + 'Tint output for the ' + + chanNames[chan] + + ' channel of pixel (' + + x + + ', ' + + y + + ') should be equivalent to multiplying the image value by tint fraction' + ); + } + } + } + } + + test('tint() with color', function() { + assert.ok(pImg, 'image loaded'); + var tintColor = [150, 100, 50, 255]; + myp5.clear(); + myp5.background(0); + myp5.tint(tintColor[0], tintColor[1], tintColor[2], tintColor[3]); + myp5.image(pImg, 0, 0); + + checkTint(tintColor); + }); + + test('tint() with alpha', function() { + assert.ok(pImg, 'image loaded'); + var tintColor = [255, 255, 255, 100]; + myp5.clear(); + myp5.background(0); + myp5.tint(tintColor[0], tintColor[1], tintColor[2], tintColor[3]); + myp5.image(pImg, 0, 0); + + checkTint(tintColor); + }); + + test('tint() with color and alpha', function() { + assert.ok(pImg, 'image loaded'); + var tintColor = [255, 100, 50, 100]; + myp5.clear(); + myp5.background(0); + myp5.tint(tintColor[0], tintColor[1], tintColor[2], tintColor[3]); + myp5.image(pImg, 0, 0); + + checkTint(tintColor); + }); +}); diff --git a/test/unit/image/p5.Image.js b/test/unit/image/p5.Image.js index eb83102d85..2600be63ff 100644 --- a/test/unit/image/p5.Image.js +++ b/test/unit/image/p5.Image.js @@ -49,4 +49,77 @@ suite('p5.Image', function() { assert.strictEqual(img.height, 30); }); }); + + suite('p5.Image.prototype.mask', function() { + test('it should mask the image', function() { + let img = myp5.createImage(10, 10); + img.loadPixels(); + for (let i = 0; i < img.height; i++) { + for (let j = 0; j < img.width; j++) { + let alpha = i < 5 ? 255 : 0; + img.set(i, j, myp5.color(0, 0, 0, alpha)); + } + } + img.updatePixels(); + + let mask = myp5.createImage(10, 10); + mask.loadPixels(); + for (let i = 0; i < mask.width; i++) { + for (let j = 0; j < mask.height; j++) { + let alpha = j < 5 ? 255 : 0; + mask.set(i, j, myp5.color(0, 0, 0, alpha)); + } + } + mask.updatePixels(); + + img.mask(mask); + img.loadPixels(); + for (let i = 0; i < img.width; i++) { + for (let j = 0; j < img.height; j++) { + let alpha = i < 5 && j < 5 ? 255 : 0; + assert.strictEqual(img.get(i, j)[3], alpha); + } + } + }); + + test('it should mask the animated gif image', function() { + const imagePath = 'unit/assets/nyan_cat.gif'; + return new Promise(function(resolve, reject) { + myp5.loadImage(imagePath, resolve, reject); + }).then(function(img) { + let mask = myp5.createImage(img.width, img.height); + mask.loadPixels(); + for (let i = 0; i < mask.width; i++) { + for (let j = 0; j < mask.height; j++) { + const alpha = j < img.height / 2 ? 255 : 0; + mask.set(i, j, myp5.color(0, 0, 0, alpha)); + } + } + mask.updatePixels(); + + img.mask(mask); + img.loadPixels(); + for (let i = 0; i < img.width; i++) { + for (let j = 0; j < img.height; j++) { + const alpha = j < img.height / 2 ? 255 : 0; + assert.strictEqual(img.get(i, j)[3], alpha); + } + } + for ( + frameIndex = 0; + frameIndex < img.gifProperties.numFrames; + frameIndex++ + ) { + const frameData = img.gifProperties.frames[frameIndex].image.data; + for (let i = 0; i < img.width; i++) { + for (let j = 0; j < img.height; j++) { + const index = 4 * (i + j * img.width) + 3; + const alpha = j < img.height / 2 ? 255 : 0; + assert.strictEqual(frameData[index], alpha); + } + } + } + }); + }); + }); }); diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index 7c317c4822..f09e141bd1 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -18,15 +18,28 @@ suite('p5.Vector', function() { }); var v; - suite('setHeading', function() { + suite('p5.prototype.setHeading() RADIANS', function() { setup(function() { + myp5.angleMode(RADIANS); v = myp5.createVector(1, 1); v.setHeading(1); }); - test('should have heading() value of 1', function() { + test('should have heading() value of 1 (RADIANS)', function() { assert.closeTo(v.heading(), 1, 0.001); }); }); + + suite('p5.prototype.setHeading() DEGREES', function() { + setup(function() { + myp5.angleMode(DEGREES); + v = myp5.createVector(1, 1); + v.setHeading(1); + }); + test('should have heading() value of 1 (DEGREES)', function() { + assert.closeTo(v.heading(), 1, 0.001); + }); + }); + suite('p5.prototype.createVector()', function() { setup(function() { v = myp5.createVector(); diff --git a/test/unit/math/trigonometry.js b/test/unit/math/trigonometry.js index 437945f471..3445baa4c3 100644 --- a/test/unit/math/trigonometry.js +++ b/test/unit/math/trigonometry.js @@ -48,17 +48,33 @@ suite('Trigonometry', function() { suite('p5.prototype.angleMode', function() { test('should set constant to DEGREES', function() { myp5.angleMode(DEGREES); - assert.equal(myp5._angleMode, 'degrees'); + assert.equal(myp5.angleMode(), 'degrees'); }); test('should set constant to RADIANS', function() { myp5.angleMode(RADIANS); - assert.equal(myp5._angleMode, 'radians'); + assert.equal(myp5.angleMode(), 'radians'); + }); + + test('wrong param type', function() { + assert.validationError(function() { + myp5.angleMode('wtflolzkk'); + }); + }); + + test('should return radians', function() { + myp5.angleMode(RADIANS); + assert.equal(myp5.angleMode(), 'radians'); + }); + + test('should return degrees', function() { + myp5.angleMode(DEGREES); + assert.equal(myp5.angleMode(), 'degrees'); }); test('should always be RADIANS or DEGREES', function() { myp5.angleMode('wtflolzkk'); - assert.equal(myp5._angleMode, 'radians'); + assert.equal(myp5.angleMode(), 'radians'); }); }); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 2195bbe843..822c3013f8 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -437,7 +437,7 @@ suite('p5.RendererGL', function() { test('blendModes change pixel colors as expected', function(done) { myp5.createCanvas(10, 10, myp5.WEBGL); myp5.noStroke(); - assert.deepEqual([133, 69, 191, 255], mixAndReturn(myp5.ADD, 255)); + assert.deepEqual([122, 0, 122, 255], mixAndReturn(myp5.ADD, 0)); assert.deepEqual([0, 0, 255, 255], mixAndReturn(myp5.REPLACE, 255)); assert.deepEqual([133, 255, 133, 255], mixAndReturn(myp5.SUBTRACT, 255)); assert.deepEqual([255, 0, 255, 255], mixAndReturn(myp5.SCREEN, 0)); @@ -447,6 +447,68 @@ suite('p5.RendererGL', function() { assert.deepEqual([0, 0, 0, 255], mixAndReturn(myp5.DARKEST, 255)); done(); }); + + test('blendModes match 2D mode', function(done) { + myp5.createCanvas(10, 10, myp5.WEBGL); + myp5.setAttributes({ alpha: true }); + const ref = myp5.createGraphics(myp5.width, myp5.height); + ref.translate(ref.width / 2, ref.height / 2); // Match WebGL mode + + const testBlend = function(target, colorA, colorB, mode) { + target.clear(); + target.push(); + target.background(colorA); + target.blendMode(mode); + target.noStroke(); + target.fill(colorB); + target.rectMode(target.CENTER); + target.rect(0, 0, target.width, target.height); + target.pop(); + return target.get(0, 0); + }; + + const assertSameIn2D = function(colorA, colorB, mode) { + const refColor = testBlend(myp5, colorA, colorB, mode); + const webglColor = testBlend(ref, colorA, colorB, mode); + if (refColor[3] === 0) { + assert.equal(webglColor[3], 0); + } else { + assert.deepEqual( + refColor, + webglColor, + `Blending ${colorA} with ${colorB} using ${mode}` + ); + } + }; + + const red = '#F53'; + const blue = '#13F'; + assertSameIn2D(red, blue, myp5.BLEND); + assertSameIn2D(red, blue, myp5.ADD); + assertSameIn2D(red, blue, myp5.DARKEST); + assertSameIn2D(red, blue, myp5.LIGHTEST); + assertSameIn2D(red, blue, myp5.EXCLUSION); + assertSameIn2D(red, blue, myp5.MULTIPLY); + assertSameIn2D(red, blue, myp5.SCREEN); + assertSameIn2D(red, blue, myp5.REPLACE); + assertSameIn2D(red, blue, myp5.REMOVE); + done(); + }); + + test('blendModes are included in push/pop', function(done) { + myp5.createCanvas(10, 10, myp5.WEBGL); + myp5.blendMode(myp5.MULTIPLY); + myp5.push(); + myp5.blendMode(myp5.ADD); + assert.equal(myp5._renderer.curBlendMode, myp5.ADD, 'Changed to ADD'); + myp5.pop(); + assert.equal( + myp5._renderer.curBlendMode, + myp5.MULTIPLY, + 'Resets to MULTIPLY' + ); + done(); + }); }); suite('BufferDef', function() { @@ -522,4 +584,171 @@ suite('p5.RendererGL', function() { }); }); }); + + suite('beginShape() in WEBGL mode', function() { + test('QUADS mode converts into triangles', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + myp5.textureMode(myp5.NORMAL); + renderer.beginShape(myp5.QUADS); + renderer.fill(255, 0, 0); + renderer.normal(0, 1, 2); + renderer.vertex(0, 0, 0, 0, 0); + renderer.fill(0, 255, 0); + renderer.normal(3, 4, 5); + renderer.vertex(0, 1, 1, 0, 1); + renderer.fill(0, 0, 255); + renderer.normal(6, 7, 8); + renderer.vertex(1, 0, 2, 1, 0); + renderer.fill(255, 0, 255); + renderer.normal(9, 10, 11); + renderer.vertex(1, 1, 3, 1, 1); + + renderer.fill(255, 0, 0); + renderer.normal(12, 13, 14); + renderer.vertex(2, 0, 4, 0, 0); + renderer.fill(0, 255, 0); + renderer.normal(15, 16, 17); + renderer.vertex(2, 1, 5, 0, 1); + renderer.fill(0, 0, 255); + renderer.normal(18, 19, 20); + renderer.vertex(3, 0, 6, 1, 0); + renderer.fill(255, 0, 255); + renderer.normal(21, 22, 23); + renderer.vertex(3, 1, 7, 1, 1); + renderer.endShape(); + + const expectedVerts = [ + [0, 0, 0], + [0, 1, 1], + [1, 0, 2], + + [0, 0, 0], + [1, 0, 2], + [1, 1, 3], + + [2, 0, 4], + [2, 1, 5], + [3, 0, 6], + + [2, 0, 4], + [3, 0, 6], + [3, 1, 7] + ]; + assert.equal( + renderer.immediateMode.geometry.vertices.length, + expectedVerts.length + ); + expectedVerts.forEach(function([x, y, z], i) { + assert.equal(renderer.immediateMode.geometry.vertices[i].x, x); + assert.equal(renderer.immediateMode.geometry.vertices[i].y, y); + assert.equal(renderer.immediateMode.geometry.vertices[i].z, z); + }); + + const expectedUVs = [ + [0, 0], + [0, 1], + [1, 0], + + [0, 0], + [1, 0], + [1, 1], + + [0, 0], + [0, 1], + [1, 0], + + [0, 0], + [1, 0], + [1, 1] + ].flat(); + assert.deepEqual(renderer.immediateMode.geometry.uvs, expectedUVs); + + const expectedColors = [ + [1, 0, 0, 1], + [0, 1, 0, 1], + [0, 0, 1, 1], + + [1, 0, 0, 1], + [0, 0, 1, 1], + [1, 0, 1, 1], + + [1, 0, 0, 1], + [0, 1, 0, 1], + [0, 0, 1, 1], + + [1, 0, 0, 1], + [0, 0, 1, 1], + [1, 0, 1, 1] + ].flat(); + assert.deepEqual( + renderer.immediateMode.geometry.vertexColors, + expectedColors + ); + + const expectedNormals = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + + [0, 1, 2], + [6, 7, 8], + [9, 10, 11], + + [12, 13, 14], + [15, 16, 17], + [18, 19, 20], + + [12, 13, 14], + [18, 19, 20], + [21, 22, 23] + ]; + assert.equal( + renderer.immediateMode.geometry.vertexNormals.length, + expectedNormals.length + ); + expectedNormals.forEach(function([x, y, z], i) { + assert.equal(renderer.immediateMode.geometry.vertexNormals[i].x, x); + assert.equal(renderer.immediateMode.geometry.vertexNormals[i].y, y); + assert.equal(renderer.immediateMode.geometry.vertexNormals[i].z, z); + }); + + done(); + }); + + test('QUADS mode makes edges for quad outlines', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + renderer.beginShape(myp5.QUADS); + renderer.vertex(0, 0); + renderer.vertex(0, 1); + renderer.vertex(1, 0); + renderer.vertex(1, 1); + + renderer.vertex(2, 0); + renderer.vertex(2, 1); + renderer.vertex(3, 0); + renderer.vertex(3, 1); + renderer.endShape(); + + assert.equal(renderer.immediateMode.geometry.edges.length, 8); + done(); + }); + + test('QUAD_STRIP mode makes edges for strip outlines', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + renderer.beginShape(myp5.QUAD_STRIP); + renderer.vertex(0, 0); + renderer.vertex(0, 1); + renderer.vertex(1, 0); + renderer.vertex(1, 1); + renderer.vertex(2, 0); + renderer.vertex(2, 1); + renderer.vertex(3, 0); + renderer.vertex(3, 1); + renderer.endShape(); + + // Two full quads (2 * 4) plus two edges connecting them + assert.equal(renderer.immediateMode.geometry.edges.length, 10); + done(); + }); + }); }); diff --git a/translations/ko/README.md b/translations/ko/README.md index 4603f10aa9..c15844116f 100644 --- a/translations/ko/README.md +++ b/translations/ko/README.md @@ -1,15 +1,26 @@ # Welcome to the FES Korean branch! μ•ˆλ…•ν•˜μ„Έμš”, FES ν•œκ΅­μ–΄ λΈŒλžœμΉ˜μ— μ–΄μ„œμ˜€μ„Έμš”! -## ν•œκ΅­μ–΄ Translation Credits +## ν•œκ΅­μ–΄ 곡동 λ²ˆμ—­ κΈ°μ—¬μž Korean Translation Credits 2021λ…„ 가을뢀터 κ³΅λ™μž‘μ—…μœΌλ‘œ μ§„ν–‰λ˜μ–΄ 2022λ…„ 1월에 마무리된 FES μ—λŸ¬λ©”μ‹œμ§€ 곡동 λ²ˆμ—­ μž‘μ—…μ€ μ•„λž˜ 뢄듀이 ν•¨κ»˜ν•˜μ…¨μŠ΅λ‹ˆλ‹€. * [염인화](https://yinhwa.art/) (Inhwa Yeom): artist/XR researcher based in South Korea. (Take a look at her works on [p5 for 50+](https://p5for50.plus/) ([Processing Foundation Fellows 2020](https://medium.com/processing-foundation/p5-js-for-ages-50-in-korea-50d47b5927fb)) and p5js website Korean translation) * μ „μœ μ§„ (Youjin Jeon): artist/organizer based in Seoul, South Korea. [여성을 μœ„ν•œ μ—΄λ¦° 기술랩(Woman Open Tech Lab.kr)](http://womanopentechlab.kr/) and [Seoul Express](http://seoulexpress.kr/) * [μ •μ•Ž](https://www.almichu.com/) (Alm Chung, organizer): Korean-American artist/researcher based in Seattle, WA. * μ΄μ§€ν˜„ (Jihyun Lee): Korean publishing editor based in South Korea -## ν•œκ΅­μ–΄ Translation Resources -* μΆ”ν›„ 좔가될 μ˜ˆμ •μž…λ‹ˆλ‹€! +## μ˜ν•œ λ²ˆμ—­ λ¦¬μ†ŒμŠ€ (Korean-English Translation Resources) +* μ˜ν•œ [λ²ˆμ—­μ— 도움이 λ˜λŠ” 툴과 μœ μ˜μ λ“€]μž…λ‹ˆλ‹€. +* λ˜ν•œ μ˜ν•œ [λ²ˆμ—­ μž‘μ—… 쀑 λ§ˆμ£ΌμΉ˜λŠ” λ”œλ ˆλ§ˆλ“€] μ†μ—μ„œ 저희가 μ±„νƒν•œ 방식을 λͺ¨μ•„ μ μ–΄λ΄€μŠ΅λ‹ˆλ‹€. +* μ™Έλž˜ [기술 μš©μ–΄ 닀루기]에 λŒ€ν•œ λ…Όμ˜μž…λ‹ˆλ‹€. +* p5js μ›Ήμ‚¬μ΄νŠΈμ™€ 기술 λ¬Έμ„œμ—μ„œ μ‚¬μš©ν•˜λŠ” 기술 μš©μ–΄λ“€μ„ ν†΅μΌν•˜κΈ°μœ„ν•΄ μ‚¬μš©ν•˜κ³  있 [p5js.org/ko 기술 μš©μ–΄ 색인] μž…λ‹ˆλ‹€. +* ν˜„μ‘΄ν•˜λŠ” κ²€μƒ‰νˆ΄/λ²ˆμ—­ νˆ΄λ“€κ³Ό 연계 κ°€λŠ₯ν•œ "[사이λ₯Ό λ§΄λ„λŠ”]" λ²ˆμ—­λ¬Έμ— λŒ€ν•΄ μƒκ°ν•΄λ³΄λŠ” κΈ€μž…λ‹ˆλ‹€. +이 외에도 FES의 세계화 μž‘μ—… κ³Όμ •, 그리고 κ³Όμ • 쀑 λ…Όμ˜λœ μ΄μŠˆλ“€μ„ [Friendly Errors i18n Book ✎ μΉœμ ˆν•œ 였λ₯˜ λ©”μ‹œμ§€ 세계화 κ°€μ΄λ“œλΆ]μ—μ„œ 읽어보싀 수 μžˆμŠ΅λ‹ˆλ‹€. "μΉœμ ˆν•œ 였λ₯˜ λ©”μ‹œμ§€ 세계화 κ°€μ΄λ“œλΆ"은 μ˜€ν”ˆ μ†ŒμŠ€ ν”„λ‘œμ νŠΈμ΄λ©°, 이 [λ…λ¦½λœ λ ˆνŒŒμ§€ν† λ¦¬ (repository)]λ₯Ό 톡해 κΈ°μ—¬ κ°€λŠ₯ν•©λ‹ˆλ‹€. -μ§ˆλ¬Έμ΄λ‚˜ 건의 사항은 @almchung μ—κ²Œ λ¬Έμ˜μ£Όμ‹œκΈΈ λ°”λžλ‹ˆλ‹€. \ No newline at end of file + +[λ²ˆμ—­μ— 도움이 λ˜λŠ” 툴과 μœ μ˜μ λ“€]: https://almchung.github.io/p5-fes-i18n-book/ch4/#tools +[λ²ˆμ—­ μž‘μ—… 쀑 λ§ˆμ£ΌμΉ˜λŠ” λ”œλ ˆλ§ˆλ“€]: https://almchung.github.io/p5-fes-i18n-book/ch4/#dilemmas +[기술 μš©μ–΄ 닀루기]: https://almchung.github.io/p5-fes-i18n-book/ch3/ +[사이λ₯Ό λ§΄λ„λŠ”]: https://almchung.github.io/p5-fes-i18n-book/ch5/ +[Friendly Errors i18n Book ✎ μΉœμ ˆν•œ 였λ₯˜ λ©”μ‹œμ§€ 세계화 κ°€μ΄λ“œλΆ]: https://almchung.github.io/p5-fes-i18n-book/ +[λ…λ¦½λœ λ ˆνŒŒμ§€ν† λ¦¬ (repository)]: https://github.com/almchung/p5-fes-i18n-book \ No newline at end of file