diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..a5e406f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,43 @@ +name: Bug Report +description: Report an issue or a bug. +title: "[BUG]: << Please use a comprehensive title... >>" +labels: [ Defect ] + +body: + - type: markdown + attributes: + value: > + Thank you for taking the time to file a bug report. Before continuing, please take some time to check the existing [issues](https://github.com/colour-science/colour-visuals/issues). + The issue could already be fixed in the [develop](https://github.com/colour-science/colour-visuals) branch. If you have an installation problem, the [installation guide](https://www.colour-science.org/installation-guide/) describes the recommended process. + + - type: textarea + attributes: + label: "Description" + description: > + Please describe the issue in a few short sentences. + validations: + required: true + + - type: textarea + attributes: + label: "Code for Reproduction" + description: > + If possible, please provide a minimum self-contained example reproducing the issue. + placeholder: | + << Your code here... >> + render: python + + - type: textarea + attributes: + label: "Exception Message" + description: > + If any, please paste the *full* exception message. + placeholder: | + << Full traceback starting from `Traceback (most recent call last):`... >> + render: shell + + - type: textarea + attributes: + label: "Environment Information" + description: If possible, please paste the output from `import colour; colour.utilities.describe_environment()`. + render: shell diff --git a/.github/ISSUE_TEMPLATE/documentation-improvement.yml b/.github/ISSUE_TEMPLATE/documentation-improvement.yml new file mode 100644 index 0000000..3680b71 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation-improvement.yml @@ -0,0 +1,40 @@ +name: Documentation Improvement +description: Report a documentation improvement. +title: "[DOCUMENTATION]: << Please use a comprehensive title... >>" +labels: [ Documentation ] + +body: + - type: markdown + attributes: + value: > + Thank you for taking the time to file a documentation improvement report. Before continuing, please take some time to check the existing [issues](https://github.com/colour-science/colour-visuals/issues). + + - type: input + attributes: + label: Documentation Link + description: > + Please link to any documentation or examples that you are referencing. Suggested improvements should be based on the [development version of the documentation](https://colour-visuals.readthedocs.io/en/develop/). + placeholder: > + << https://colour-visuals.readthedocs.io/en/develop/... >> + validations: + required: true + + - type: textarea + attributes: + label: Description + description: > + Please describe what is missing, unclear or incorrect. + validations: + required: true + + - type: textarea + attributes: + label: Suggested Improvement + description: > + Please describe how the documentation could be improved. + + - type: textarea + attributes: + label: "Environment Information" + description: If possible, please paste the output from `import colour; colour.utilities.describe_environment()`. + render: shell diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..6ead72d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,18 @@ +name: Feature Request +description: Suggest a new feature to implement. +title: "[FEATURE]: << Please use a comprehensive title... >>" +labels: [ Feature ] + +body: + - type: markdown + attributes: + value: > + Thank you for taking the time to file a feature request. Before continuing, please take some time to check the existing [issues](https://github.com/colour-science/colour-visuals/issues) and also the [draft release notes](https://gist.github.com/KelSolaar/4a6ebe9ec3d389f0934b154fec8df51d). + + - type: textarea + attributes: + label: "Description" + description: > + Please describe the new feature in a few short sentences. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..12dbeca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,17 @@ +name: Question +description: Ask a question. +title: "[DISCUSSION]: << Please use a comprehensive title... >>" +labels: [ Discussion ] + +body: + - type: markdown + attributes: + value: Thank you for taking the time to ask a question or discuss. Before continuing, we would be glad if you were to start this discussion in the dedicated [discussions](https://github.com/colour-science/colour-visuals/discussions) area. + + - type: textarea + attributes: + label: "Question" + description: > + If you are still here, please consider using the dedicated [discussions](https://github.com/colour-science/colour-visuals/discussions) area. + placeholder: > + << The discussions area is this way: https://github.com/colour-science/colour-visuals/discussions... >> diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..32e3642 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ + + +# Summary + + + +# Preflight + + + +**Code Style and Quality** + +- [ ] Unit tests have been implemented and passed. +- [ ] Pyright static checking has been run and passed. +- [ ] Pre-commit hooks have been run and passed. + + + + +**Documentation** + +- [ ] New features are documented along with examples if relevant. +- [ ] The documentation is [Sphinx](https://www.sphinx-doc.org/en/master/) and [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant. + + diff --git a/.github/funding.yml b/.github/funding.yml new file mode 100644 index 0000000..2614032 --- /dev/null +++ b/.github/funding.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: colour-science # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/continuous-integration-documentation.yml b/.github/workflows/continuous-integration-documentation.yml new file mode 100644 index 0000000..6877a5a --- /dev/null +++ b/.github/workflows/continuous-integration-documentation.yml @@ -0,0 +1,48 @@ +name: Continuous Integration - Documentation + +on: [push, pull_request] + +jobs: + continuous-integration-documentation: + name: ${{ matrix.os }} - Python ${{ matrix.python-version }} + strategy: + matrix: + os: [ubuntu-22.04] + python-version: [3.11] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v1 + - name: Environment Variables + run: | + echo "CI_PYTHON_VERSION=${{ matrix.python-version }}" >> $GITHUB_ENV + echo "CI_PACKAGE=colour_visuals" >> $GITHUB_ENV + echo "CI_SHA=${{ github.sha }}" >> $GITHUB_ENV + echo "MPLBACKEND=AGG" >> $GITHUB_ENV + echo "COLOUR_SCIENCE__DOCUMENTATION_BUILD=True" >> $GITHUB_ENV + shell: bash + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get --yes install latexmk texlive-full + - name: Install Poetry + env: + POETRY_VERSION: 1.4.0 + run: | + curl -sSL https://install.python-poetry.org | POETRY_HOME=$HOME/.poetry python3 - + echo "$HOME/.poetry/bin" >> $GITHUB_PATH + shell: bash + - name: Install Package Dependencies + run: | + poetry run python -m pip install --upgrade pip + poetry install + poetry run python -c "import imageio;imageio.plugins.freeimage.download()" + shell: bash + - name: Build Documentation + run: | + poetry run invoke docs + shell: bash diff --git a/.github/workflows/continuous-integration-quality-unit-tests.yml b/.github/workflows/continuous-integration-quality-unit-tests.yml new file mode 100644 index 0000000..2d3ca71 --- /dev/null +++ b/.github/workflows/continuous-integration-quality-unit-tests.yml @@ -0,0 +1,62 @@ +name: Continuous Integration - Quality & Unit Tests + +on: [push, pull_request] + +jobs: + continuous-integration-quality-unit-tests: + name: ${{ matrix.os }} - Python ${{ matrix.python-version }} + strategy: + matrix: + os: [macOS-latest, ubuntu-22.04, windows-latest] + python-version: [3.9, '3.10', 3.11] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - name: Environment Variables + run: | + echo "CI_PYTHON_VERSION=${{ matrix.python-version }}" >> $GITHUB_ENV + echo "CI_PACKAGE=colour_visuals" >> $GITHUB_ENV + echo "CI_SHA=${{ github.sha }}" >> $GITHUB_ENV + echo "COVERALLS_REPO_TOKEN=${{ secrets.COVERALLS_REPO_TOKEN }}" >> $GITHUB_ENV + shell: bash + - name: Set up Python 3.9 for Pre-Commit + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install Poetry + env: + POETRY_VERSION: 1.4.0 + run: | + curl -sSL https://install.python-poetry.org | POETRY_HOME=$HOME/.poetry python3 - + echo "$HOME/.poetry/bin" >> $GITHUB_PATH + shell: bash + - name: Install Package Dependencies + run: | + poetry run python -m pip install --upgrade pip + poetry install + poetry run python -c "import imageio;imageio.plugins.freeimage.download()" + shell: bash + - name: Pre-Commit (All Files) + run: | + poetry run pre-commit run --all-files + shell: bash + - name: Test Optimised Python Execution + run: | + poetry run python -OO -c "import $CI_PACKAGE" + shell: bash + - name: Test with Pytest + run: | + poetry run python -W ignore -m pytest --doctest-modules --ignore=$CI_PACKAGE/examples --cov=$CI_PACKAGE $CI_PACKAGE + shell: bash + - name: Upload Coverage to coveralls.io + if: matrix.os == 'macOS-latest' && matrix.python-version == '3.11' + run: | + if [ -z "$COVERALLS_REPO_TOKEN" ]; then echo \"COVERALLS_REPO_TOKEN\" secret is undefined!; else poetry run coveralls; fi + shell: bash diff --git a/.github/workflows/continuous-integration-static-type-checking.yml b/.github/workflows/continuous-integration-static-type-checking.yml new file mode 100644 index 0000000..91c5a17 --- /dev/null +++ b/.github/workflows/continuous-integration-static-type-checking.yml @@ -0,0 +1,29 @@ +name: Continuous Integration - Static Type Checking + +on: [push, pull_request] + +jobs: + continuous-integration-static-type-checking: + name: ${{ matrix.os }} - Python ${{ matrix.python-version }} + strategy: + matrix: + os: [macOS-latest] + python-version: [3.11] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v1 + - name: Environment Variables + run: | + echo "CI_PACKAGE=colour_visuals" >> $GITHUB_ENV + shell: bash + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install Package Dependencies + run: | + pip install -r requirements.txt + - name: Static Type Checking + run: | + pyright --skipunannotated diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acffe21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*.egg-info +*.pyc +*.pyo +.DS_Store +.coverage* +.fleet +.idea +.ipynb_checkpoints +.sandbox +.vs +.vscode + +__pycache__ + +build +colour_visuals.egg-info +dist +docs/_build +docs/_static/Examples_*.png +docs/_static/Plotting_*.png +docs/generated +poetry.lock diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b287c09 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "colour_visuals/resources/colour-visuals-tests-datasets"] + path = colour_visuals/resources/colour-visuals-tests-datasets + url = https://github.com/colour-science/colour-visuals-tests-datasets.git +[submodule "colour_visuals/resources/colour-visuals-examples-datasets"] + path = colour_visuals/resources/colour-visuals-examples-datasets + url = https://github.com/colour-science/colour-visuals-examples-datasets.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..570a31c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +repos: +- repo: https://github.com/ikamensh/flynt/ + rev: '1.0.1' + hooks: + - id: flynt +- repo: https://github.com/PyCQA/isort + rev: '5.12.0' + hooks: + - id: isort +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: 'v0.0.285' + hooks: + - id: ruff +- repo: https://github.com/psf/black + rev: 23.7.0 + hooks: + - id: black + language_version: python3.9 +- repo: https://github.com/keewis/blackdoc + rev: v0.3.8 + hooks: + - id: blackdoc + language_version: python3.9 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..c93eb8a --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +version: 2 + +build: + os: ubuntu-20.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + +formats: + - htmlzip + - pdf + +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..67ab8e5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,51 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others’ private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting Thomas Mansencal and Michael Mauderer via email at thomas@colour-science.org and michael@colour-science.org respectively. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][homepage]. + +For answers to common questions about this code of conduct, see [https://www.contributor-covenant.org/faq][faq]. + + +[homepage]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +[faq]: https://www.contributor-covenant.org/faq \ No newline at end of file diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst new file mode 100644 index 0000000..b5bd688 --- /dev/null +++ b/CONTRIBUTORS.rst @@ -0,0 +1,20 @@ +Contributors +============ + +Development & Technical Support +------------------------------- + +- **Thomas Mansencal**, *Technology Supervisor @ Wētā FX* + + Project coordination, overall development. + +Issues & Discussions +-------------------- + +About +----- + +| **Colour - Visuals** by Colour Developers +| Copyright 2023 Colour Developers – `colour-developers@colour-science.org `__ +| This software is released under terms of BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause +| `https://github.com/colour-science/colour-visuals `__ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..83ed44a --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +Copyright 2023 Colour Developers + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8addf40 --- /dev/null +++ b/README.rst @@ -0,0 +1,89 @@ +Colour - Visuals +================ + +.. start-badges + +|actions| |coveralls| |codacy| |version| + +.. |actions| image:: https://img.shields.io/github/actions/workflow/status/colour-science/colour-visuals/.github/workflows/continuous-integration-quality-unit-tests.yml?branch=develop&style=flat-square + :target: https://github.com/colour-science/colour-visuals/actions + :alt: Develop Build Status +.. |coveralls| image:: http://img.shields.io/coveralls/colour-science/colour-visuals/develop.svg?style=flat-square + :target: https://coveralls.io/r/colour-science/colour-visuals + :alt: Coverage Status +.. |codacy| image:: https://img.shields.io/codacy/grade/2862b4f2217742ae83c972d7e3af44d7/develop.svg?style=flat-square + :target: https://www.codacy.com/app/colour-science/colour-visuals + :alt: Code Grade +.. |version| image:: https://img.shields.io/pypi/v/colour-visuals.svg?style=flat-square + :target: https://pypi.org/project/colour-visuals + :alt: Package Version + +.. end-badges + +A `Python `__ package implementing various +`WebGPU-based `__ visuals for colour science applications. + +It is open source and freely available under the +`BSD-3-Clause `__ terms. + +.. contents:: **Table of Contents** + :backlinks: none + :depth: 2 + +.. sectnum:: + +Features +-------- + +Examples +^^^^^^^^ + +User Guide +---------- + +Installation +^^^^^^^^^^^^ + +Primary Dependencies +~~~~~~~~~~~~~~~~~~~~ + +Pypi +~~~~ + +Contributing +^^^^^^^^^^^^ + +If you would like to contribute to `Colour - Visuals `__, +please refer to the following `Contributing `__ +guide for `Colour `__. + +API Reference +------------- + +The main technical reference for `Colour - Visuals `__ +is the `API Reference `__. + +Code of Conduct +--------------- + +The *Code of Conduct*, adapted from the `Contributor Covenant 1.4 `__, +is available on the `Code of Conduct `__ page. + +Contact & Social +---------------- + +The *Colour Developers* can be reached via different means: + +- `Email `__ +- `Facebook `__ +- `Github Discussions `__ +- `Gitter `__ +- `Twitter `__ + +About +----- + +| **Colour - Visuals** by Colour Developers +| Copyright 2023 Colour Developers – `colour-developers@colour-science.org `__ +| This software is released under terms of BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause +| `https://github.com/colour-science/colour-visuals `__ diff --git a/TODO.rst b/TODO.rst new file mode 100644 index 0000000..d7f4d01 --- /dev/null +++ b/TODO.rst @@ -0,0 +1,13 @@ +Colour - Visuals - TODO +=========================== + +TODO +---- + +About +----- + +| **Colour - Visuals** by Colour Developers +| Copyright 2023 Colour Developers - `colour-developers@colour-science.org `__ +| This software is released under terms of BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause +| `https://github.com/colour-science/colour-visuals `__ diff --git a/colour_visuals/__init__.py b/colour_visuals/__init__.py new file mode 100644 index 0000000..a761f66 --- /dev/null +++ b/colour_visuals/__init__.py @@ -0,0 +1,105 @@ +""" +Colour - Visuals +================ + +WebGPU-based visuals for colour science applications. +""" + +from __future__ import annotations + +import contextlib +import os +import subprocess + +import colour +import numpy as np + +from .diagrams import ( + VisualChromaticityDiagram, + VisualChromaticityDiagramCIE1931, + VisualChromaticityDiagramCIE1960UCS, + VisualChromaticityDiagramCIE1976UCS, + VisualSpectralLocus2D, + VisualSpectralLocus3D, +) +from .grid import ( + VisualGrid, +) +from .pointer_gamut import ( + VisualPointerGamut2D, + VisualPointerGamut3D, +) +from .rgb_colourspace import ( + VisualRGBColourspace2D, + VisualRGBColourspace3D, +) +from .rgb_scatter import ( + VisualRGBScatter3D, +) +from .rosch_macadam import ( + VisualRoschMacAdam, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "VisualSpectralLocus2D", + "VisualSpectralLocus3D", + "VisualChromaticityDiagram", + "VisualChromaticityDiagramCIE1931", + "VisualChromaticityDiagramCIE1960UCS", + "VisualChromaticityDiagramCIE1976UCS", +] +__all__ += [ + "VisualGrid", +] +__all__ += [ + "VisualPointerGamut2D", + "VisualPointerGamut3D", +] +__all__ += [ + "VisualRGBColourspace2D", + "VisualRGBColourspace3D", +] +__all__ += [ + "VisualRGBScatter3D", +] +__all__ += [ + "VisualRoschMacAdam", +] +__application_name__ = "Colour - Visuals" + +__major_version__ = "0" +__minor_version__ = "1" +__change_version__ = "0" +__version__ = ".".join( + (__major_version__, __minor_version__, __change_version__) +) + +try: + _version: str = ( + subprocess.check_output( + ["git", "describe"], # noqa: S603, S607 + cwd=os.path.dirname(__file__), + stderr=subprocess.STDOUT, + ) + .strip() + .decode("utf-8") + ) +except Exception: + _version: str = __version__ + +colour.utilities.ANCILLARY_COLOUR_SCIENCE_PACKAGES[ # pyright: ignore + "colour-visuals" +] = _version + +del _version + +# TODO: Remove legacy printing support when deemed appropriate. +with contextlib.suppress(TypeError): + np.set_printoptions(legacy="1.13") diff --git a/colour_visuals/common.py b/colour_visuals/common.py new file mode 100644 index 0000000..8c7d582 --- /dev/null +++ b/colour_visuals/common.py @@ -0,0 +1,167 @@ +""" +Common Utilities +================ + +Defines the common utilities objects that don't fall in any specific category. +""" + +from __future__ import annotations + +import numpy as np +from colour.graph import convert +from colour.hints import ArrayLike, LiteralColourspaceModel, NDArray, Tuple +from colour.models import ( + XYZ_to_ICtCp, + XYZ_to_Jzazbz, + XYZ_to_OSA_UCS, +) +from colour.utilities import full + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "DEFAULT_FLOAT_DTYPE_WGPU", + "DEFAULT_INT_DTYPE_WGPU", + "XYZ_to_colourspace_model", + "as_contiguous_array", + "conform_primitive_dtype", + "append_channel", +] + +DEFAULT_FLOAT_DTYPE_WGPU = np.float32 +"""Default int number dtype.""" + +DEFAULT_INT_DTYPE_WGPU = np.uint32 +"""Default floating point number dtype.""" + + +def XYZ_to_colourspace_model( + XYZ: ArrayLike, + illuminant: ArrayLike, + model: LiteralColourspaceModel | str = "CIE xyY", + **kwargs, +) -> NDArray: + """ + Convert from *CIE XYZ* tristimulus values to given colourspace model while + normalising some of the absolute models. + + Parameters + ---------- + XYZ + *CIE XYZ* tristimulus values to convert to given colourspace model. + illuminant + Reference *illuminant* *CIE xy* chromaticity coordinates or *CIE xyY* + colourspace array. + model + Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for + the list of supported colourspace models. + """ + + ijk = convert( + XYZ, + "CIE XYZ", + model, + illuminant=illuminant, + verbose={"mode": "Short"}, + **kwargs, + ) + + if model == "ICtCp": + ijk /= XYZ_to_ICtCp([1, 1, 1])[0] + elif model == "JzAzBz": + ijk /= XYZ_to_Jzazbz([1, 1, 1])[0] + elif model == "OSA UCS": + ijk /= XYZ_to_OSA_UCS([1, 1, 1])[0] + + return ijk + + +def as_contiguous_array(a, dtype=DEFAULT_FLOAT_DTYPE_WGPU): + """ + Convert given array to a contiguous array (ndim >= 1) in memory (C order). + + Parameters + ---------- + a + Variable :math:`a` to convert. + dtype + :class:`numpy.dtype` to use for conversion, default to the + :class:`numpy.dtype` defined by the + :attr:`colour.constant.DEFAULT_FLOAT_DTYPE_WGPU` attribute. + + Returns + ------- + :class:`numpy.ndarray` + Converted variable :math:`a`. + """ + + return np.ascontiguousarray(a.astype(dtype)) + + +def conform_primitive_dtype( + primitive: Tuple[NDArray, NDArray, NDArray] +) -> Tuple[NDArray, NDArray, NDArray]: + """ + Conform the given primitive to the required *WebGPU* dtype. + + Parameters + ---------- + primitive + Primitive to conform the dtype of. + + Returns + ------- + :class:`numpy.ndarray` + Conformed primitive. + """ + + vertices, faces, outline = primitive + + return ( + vertices.astype( + [ + ("position", DEFAULT_FLOAT_DTYPE_WGPU, (3,)), + ("uv", DEFAULT_FLOAT_DTYPE_WGPU, (2,)), + ("normal", DEFAULT_FLOAT_DTYPE_WGPU, (3,)), + ("colour", DEFAULT_FLOAT_DTYPE_WGPU, (4,)), + ] + ), + faces.astype(DEFAULT_INT_DTYPE_WGPU), + outline.astype(DEFAULT_INT_DTYPE_WGPU), + ) + + +def append_channel(a: ArrayLike, value: float = 1) -> NDArray: + """ + Append a channel to given variable :math:`a`. + + Parameters + ---------- + a + Variable :math:`a` to append a channel to. + value + Channel value. + + Returns + ------- + :class:`numpy.ndarray` + Variable :math:`a` with appended channel. + """ + + a = np.copy(a) + + return np.hstack( # pyright: ignore + [ + a, + full( + (*list(a.shape[:-1]), 1), + value, + dtype=a.dtype, # pyright: ignore + ), + ] + ) diff --git a/colour_visuals/diagrams.py b/colour_visuals/diagrams.py new file mode 100644 index 0000000..4949e3a --- /dev/null +++ b/colour_visuals/diagrams.py @@ -0,0 +1,705 @@ +# !/usr/bin/env python +""" +Chromaticity Diagram Visuals +============================ + +Defines the *Chromaticity Diagram* visuals: + +- :class:`colour_visuals.VisualSpectralLocus2D` +- :class:`colour_visuals.VisualSpectralLocus3D` +- :class:`colour_visuals.VisualChromaticityDiagram` +- :class:`colour_visuals.VisualChromaticityDiagramCIE1931` +- :class:`colour_visuals.VisualChromaticityDiagramCIE1960UCS` +- :class:`colour_visuals.VisualChromaticityDiagramCIE1976UCS` +""" + +from __future__ import annotations + +import numpy as np +import pygfx as gfx +from colour.algebra import euclidean_distance, normalise_maximum +from colour.colorimetry import MultiSpectralDistributions +from colour.hints import ( + ArrayLike, + Literal, + LiteralColourspaceModel, + Sequence, + Type, + cast, +) +from colour.models import XYZ_to_RGB +from colour.plotting import ( + CONSTANTS_COLOUR_STYLE, + LABELS_CHROMATICITY_DIAGRAM_DEFAULT, + METHODS_CHROMATICITY_DIAGRAM, + XYZ_to_plotting_colourspace, + colourspace_model_axis_reorder, + filter_cmfs, +) +from colour.plotting.diagrams import lines_spectral_locus +from colour.utilities import ( + first_item, + full, + optional, + tstack, + validate_method, +) +from scipy.spatial import Delaunay + +from colour_visuals.common import ( + DEFAULT_FLOAT_DTYPE_WGPU, + DEFAULT_INT_DTYPE_WGPU, + XYZ_to_colourspace_model, + append_channel, + as_contiguous_array, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "VisualSpectralLocus2D", + "VisualSpectralLocus3D", + "VisualChromaticityDiagram", + "VisualChromaticityDiagramCIE1931", + "VisualChromaticityDiagramCIE1960UCS", + "VisualChromaticityDiagramCIE1976UCS", +] + + +class VisualSpectralLocus2D(gfx.Group): + """ + Create a 2D *Spectral Locus* visual. + + Parameters + ---------- + cmfs + Standard observer colour matching functions used for computing the + spectrum domain and colours. ``cmfs`` can be of any type or form + supported by the :func:`colour.plotting.common.filter_cmfs` definition. + method + *Chromaticity Diagram* method. + labels + Array of wavelength labels used to customise which labels will be drawn + around the spectral locus. Passing an empty array will result in no + wavelength labels being drawn. + colours + Colours of the visual, if *None*, the colours are computed from the + visual geometry. + opacity + Opacity of the visual. + thickness + Thickness of the visual lines. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualSpectralLocus2D() + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualSpectralLocus2D.png + :align: center + :alt: visual-spectral-locus-2d + """ + + def __init__( + self, + cmfs: MultiSpectralDistributions + | str + | Sequence[ + MultiSpectralDistributions | str + ] = "CIE 1931 2 Degree Standard Observer", + method: Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] + | str = "CIE 1931", + labels: Sequence | None = None, + colours: ArrayLike | None = None, + opacity: float = 1, + thickness: float = 1, + ): + super().__init__() + + cmfs = cast( + MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values()) + ) + method = validate_method(method, tuple(METHODS_CHROMATICITY_DIAGRAM)) + labels = cast( + Sequence, + optional(labels, LABELS_CHROMATICITY_DIAGRAM_DEFAULT[method]), + ) + + lines_sl, lines_w = lines_spectral_locus(cmfs, labels, method) + + # Spectral Locus + positions = np.concatenate( + [lines_sl["position"][:-1], lines_sl["position"][1:]], axis=1 + ).reshape([-1, 2]) + + positions = np.hstack( + [ + positions, + np.full((positions.shape[0], 1), 0, DEFAULT_FLOAT_DTYPE_WGPU), + ] + ) + + if colours is None: + colours_sl = np.concatenate( + [lines_sl["colour"][:-1], lines_sl["colour"][1:]], axis=1 + ).reshape([-1, 3]) + else: + colours_sl = np.tile(colours, (positions.shape[0], 1)) + + self._spectral_locus = gfx.Line( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array( + append_channel(colours_sl, opacity) + ), + ), + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + self.add(self._spectral_locus) + + # Wavelengths + positions = lines_w["position"] + positions = np.hstack( + [ + positions, + np.full((positions.shape[0], 1), 0, DEFAULT_FLOAT_DTYPE_WGPU), + ] + ) + + if colours is None: + colours_w = lines_w["colour"] + else: + colours_w = np.tile(colours, (positions.shape[0], 1)) + + self._wavelengths = gfx.Line( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array(append_channel(colours_w, opacity)), + ), + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + self.add(self._wavelengths) + + # Labels + self._labels = [] + for i, label in enumerate( + [label for label in labels if label in cmfs.wavelengths] + ): + positions = lines_w["position"][::2] + normals = lines_w["normal"][::2] + + text = gfx.Text( + gfx.TextGeometry( + str(label), + font_size=CONSTANTS_COLOUR_STYLE.font_size.medium, + screen_space=True, + anchor="Center-Left" + if lines_w["normal"][::2][i, 0] >= 0 + else "Center-Right", + ), + gfx.TextMaterial(color=CONSTANTS_COLOUR_STYLE.colour.light), + ) + text.local.position = np.array( + [ + positions[i, 0] + normals[i, 0] * 1.5, + positions[i, 1] + normals[i, 1] * 1.5, + 0, + ] + ) + self._labels.append(text) + self.add(text) + + positions = np.hstack( + [ + lines_w["position"][::2], + np.full( + (lines_w["position"][::2].shape[0], 1), + 0, + DEFAULT_FLOAT_DTYPE_WGPU, + ), + ] + ) + + if colours is None: + colours_lp = lines_w["colour"][::2] + else: + colours_lp = np.tile(colours, (positions.shape[0], 1)) + + self._points = gfx.Points( + gfx.Geometry( + positions=as_contiguous_array(positions), + sizes=as_contiguous_array( + full(lines_w["position"][::2].shape[0], thickness * 3) + ), + colors=as_contiguous_array( + append_channel(colours_lp, opacity) + ), + ), + gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), + ) + self.add(self._points) + + +class VisualSpectralLocus3D(gfx.Line): + """ + Create a 3D *Spectral Locus* visual. + + Parameters + ---------- + cmfs + Standard observer colour matching functions used for computing the + spectrum domain and colours. ``cmfs`` can be of any type or form + supported by the :func:`colour.plotting.common.filter_cmfs` definition. + model + Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for + the list of supported colourspace models. + labels + Array of wavelength labels used to customise which labels will be drawn + around the spectral locus. Passing an empty array will result in no + wavelength labels being drawn. + colours + Colours of the visual, if *None*, the colours are computed from the + visual geometry. + opacity + Opacity of the visual. + thickness + Thickness of the visual lines. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualSpectralLocus3D(model="CIE XYZ") + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualSpectralLocus3D.png + :align: center + :alt: visual-spectral-locus-3d + """ + + def __init__( + self, + cmfs: MultiSpectralDistributions + | str + | Sequence[ + MultiSpectralDistributions | str + ] = "CIE 1931 2 Degree Standard Observer", + model: LiteralColourspaceModel | str = "CIE xyY", + colours: ArrayLike | None = None, + opacity: float = 1, + thickness: float = 1, + ): + super().__init__() + + cmfs = cast( + MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values()) + ) + + colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace + + positions = colourspace_model_axis_reorder( + XYZ_to_colourspace_model( + cmfs.values, colourspace.whitepoint, model + ), + model, + ) + positions = np.concatenate( + [positions[:-1], positions[1:]], axis=1 + ).reshape([-1, 3]) + + if colours is None: + colours = XYZ_to_RGB(cmfs.values, colourspace) + colours = np.concatenate( + [colours[:-1], colours[1:]], axis=1 + ).reshape([-1, 3]) + else: + colours = np.tile(colours, (positions.shape[0], 1)) + + super().__init__( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array(append_channel(colours, opacity)), + ), + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + + +class VisualChromaticityDiagram(gfx.Mesh): + """ + Create a *Chromaticity Diagram* visual. + + Parameters + ---------- + cmfs + Standard observer colour matching functions used for computing the + spectrum domain and colours. ``cmfs`` can be of any type or form + supported by the :func:`colour.plotting.common.filter_cmfs` definition. + method + *Chromaticity Diagram* method. + colours + Colours of the visual, if *None*, the colours are computed from the + visual geometry. + opacity + Opacity of the visual. + material + Material used to surface the visual geomeetry. + wireframe + Whether to render the visual as a wireframe, i.e., only render edges. + samples + Samples count used for generating the *Chromaticity Diagram* Delaunay + tesselation. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualChromaticityDiagram() + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualChromaticityDiagram.png + :align: center + :alt: visual-chromaticity-diagram + """ + + def __init__( + self, + cmfs: MultiSpectralDistributions + | str + | Sequence[ + MultiSpectralDistributions | str + ] = "CIE 1931 2 Degree Standard Observer", + method: Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] + | str = "CIE 1931", + colours: ArrayLike | None = None, + opacity: float = 1, + material: Type[gfx.MeshAbstractMaterial] = gfx.MeshBasicMaterial, + wireframe: bool = False, + samples: int = 64, + ): + cmfs = cast( + MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values()) + ) + + illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint + + XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[method]["XYZ_to_ij"] + ij_to_XYZ = METHODS_CHROMATICITY_DIAGRAM[method]["ij_to_XYZ"] + + # CMFS + ij_l = XYZ_to_ij(cmfs.values, illuminant) + + # Line of Purples + d = euclidean_distance(ij_l[0], ij_l[-1]) + ij_p = tstack( + [ + np.linspace(ij_l[0][0], ij_l[-1][0], int(d * samples)), + np.linspace(ij_l[0][1], ij_l[-1][1], int(d * samples)), + ] + ) + + # Grid + triangulation = Delaunay(ij_l, qhull_options="QJ") + xi = np.linspace(0, 1, samples) + ii_g, jj_g = np.meshgrid(xi, xi) + ij_g = tstack([ii_g, jj_g]) + ij_g = ij_g[triangulation.find_simplex(ij_g) > 0] + + ij = np.vstack([ij_l, illuminant, ij_p, ij_g]) + triangulation = Delaunay(ij, qhull_options="QJ") + positions = np.hstack( + [ij, np.full((ij.shape[0], 1), 0, DEFAULT_FLOAT_DTYPE_WGPU)] + ) + + if colours is None: + colours = normalise_maximum( + XYZ_to_plotting_colourspace( + ij_to_XYZ(positions[..., :2], illuminant), illuminant + ), + axis=-1, + ) + else: + colours = np.tile(colours, (positions.shape[0], 1)) + + geometry = gfx.Geometry( + positions=as_contiguous_array(positions), + indices=as_contiguous_array( + triangulation.simplices, DEFAULT_INT_DTYPE_WGPU + ), + colors=as_contiguous_array(append_channel(colours, opacity)), + ) + + super().__init__( + geometry, + material(color_mode="vertex", wireframe=wireframe) + if wireframe + else material(color_mode="vertex"), + ) + + +class VisualChromaticityDiagramCIE1931(gfx.Group): + """ + Create the *CIE 1931* *Chromaticity Diagram* visual. + + Parameters + ---------- + kwargs_visual_spectral_locus + Keyword arguments for the underlying + :class:`colour_visuals.VisualSpectralLocus2D` class. + kwargs_visual_chromaticity_diagram + Keyword arguments for the underlying + :class:`colour_visuals.VisualChromaticityDiagram` class. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualChromaticityDiagramCIE1931( + ... kwargs_visual_chromaticity_diagram={"opacity": 0.25} + ... ) + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualChromaticityDiagramCIE1931.png + :align: center + :alt: visual-chromaticity-diagram-cie-1931 + """ + + def __init__( + self, + kwargs_visual_spectral_locus: dict | None = None, + kwargs_visual_chromaticity_diagram: dict | None = None, + ): + super().__init__() + + self._spectral_locus = VisualSpectralLocus2D( + method="CIE 1931", **(optional(kwargs_visual_spectral_locus, {})) + ) + self.add(self._spectral_locus) + + self._chromaticity_diagram = VisualChromaticityDiagram( + method="CIE 1931", + **(optional(kwargs_visual_chromaticity_diagram, {})), + ) + self.add(self._chromaticity_diagram) + + +class VisualChromaticityDiagramCIE1960UCS(gfx.Group): + """ + Create the *CIE 1960 UCS* *Chromaticity Diagram* visual. + + Parameters + ---------- + kwargs_visual_spectral_locus + Keyword arguments for the underlying + :class:`colour_visuals.VisualSpectralLocus2D` class. + kwargs_visual_chromaticity_diagram + Keyword arguments for the underlying + :class:`colour_visuals.VisualChromaticityDiagram` class. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualChromaticityDiagramCIE1960UCS( + ... kwargs_visual_chromaticity_diagram={"opacity": 0.25} + ... ) + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualChromaticityDiagramCIE1960UCS.png + :align: center + :alt: visual-chromaticity-diagram-cie-1960-ucs + """ + + def __init__( + self, + kwargs_visual_spectral_locus: dict | None = None, + kwargs_visual_chromaticity_diagram: dict | None = None, + ): + super().__init__() + + self._spectral_locus = VisualSpectralLocus2D( + method="CIE 1960 UCS", + **(optional(kwargs_visual_spectral_locus, {})), + ) + self.add(self._spectral_locus) + + self._chromaticity_diagram = VisualChromaticityDiagram( + method="CIE 1960 UCS", + **(optional(kwargs_visual_chromaticity_diagram, {})), + ) + self.add(self._chromaticity_diagram) + + +class VisualChromaticityDiagramCIE1976UCS(gfx.Group): + """ + Create the *CIE 1976 UCS* *Chromaticity Diagram* visual. + + Parameters + ---------- + kwargs_visual_spectral_locus + Keyword arguments for the underlying + :class:`colour_visuals.VisualSpectralLocus2D` class. + kwargs_visual_chromaticity_diagram + Keyword arguments for the underlying + :class:`colour_visuals.VisualChromaticityDiagram` class. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualChromaticityDiagramCIE1976UCS( + ... kwargs_visual_chromaticity_diagram={"opacity": 0.25} + ... ) + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualChromaticityDiagramCIE1976UCS.png + :align: center + :alt: visual-chromaticity-diagram-cie-1976-ucs + """ + + def __init__( + self, + kwargs_visual_spectral_locus: dict | None = None, + kwargs_visual_chromaticity_diagram: dict | None = None, + ): + super().__init__() + + self._spectral_locus = VisualSpectralLocus2D( + method="CIE 1976 UCS", + **(optional(kwargs_visual_spectral_locus, {})), + ) + self.add(self._spectral_locus) + + self._chromaticity_diagram = VisualChromaticityDiagram( + method="CIE 1976 UCS", + **(optional(kwargs_visual_chromaticity_diagram, {})), + ) + self.add(self._chromaticity_diagram) + + +if __name__ == "__main__": + scene = gfx.Scene() + + scene.add( + gfx.Background( + None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ) + ) + + visual_1 = VisualChromaticityDiagramCIE1931() + scene.add(visual_1) + + visual_2 = VisualChromaticityDiagramCIE1931( + kwargs_visual_chromaticity_diagram={"wireframe": True, "opacity": 0.5} + ) + visual_2.local.position = np.array([1, 0, 0]) + scene.add(visual_2) + + visual_3 = VisualChromaticityDiagramCIE1931( + kwargs_visual_chromaticity_diagram={"colours": [0.5, 0.5, 0.5]} + ) + visual_3.local.position = np.array([2, 0, 0]) + scene.add(visual_3) + + visual_4 = VisualChromaticityDiagramCIE1960UCS() + visual_4.local.position = np.array([3, 0, 0]) + scene.add(visual_4) + + visual_5 = VisualChromaticityDiagramCIE1976UCS() + visual_5.local.position = np.array([4, 0, 0]) + scene.add(visual_5) + + visual_6 = VisualSpectralLocus2D(colours=[0.5, 0.5, 0.5]) + visual_6.local.position = np.array([5, 0, 0]) + scene.add(visual_6) + + visual_7 = VisualSpectralLocus3D() + scene.add(visual_7) + + visual_8 = VisualSpectralLocus3D(colours=[0.5, 0.5, 0.5]) + visual_8.local.position = np.array([5, 0, 0]) + scene.add(visual_8) + + visual_9 = VisualSpectralLocus3D(model="CIE XYZ") + visual_9.local.position = np.array([6, 0, 0]) + scene.add(visual_9) + + gfx.show(scene, up=np.array([0, 0, 1])) diff --git a/colour_visuals/grid.py b/colour_visuals/grid.py new file mode 100644 index 0000000..7c58297 --- /dev/null +++ b/colour_visuals/grid.py @@ -0,0 +1,259 @@ +# !/usr/bin/env python +""" +Grid Visuals +============ + +Defines the grid visuals: + +- :class:`colour_visuals.VisualGrid` +""" + +from __future__ import annotations + +import numpy as np +import pygfx as gfx +from colour.geometry import primitive_grid +from colour.hints import ArrayLike +from colour.plotting import CONSTANTS_COLOUR_STYLE + +from colour_visuals.common import ( + DEFAULT_FLOAT_DTYPE_WGPU, + append_channel, + as_contiguous_array, + conform_primitive_dtype, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = ["VisualGrid"] + + +class VisualGrid(gfx.Group): + """ + Create a 3D grid. + + Parameters + ---------- + size + Size of the grid. + major_grid_colours + Colours of the major grid lines. + minor_grid_colours + Colours of the minor grid lines. + major_tick_labels + Whether to draw the major tick labels. + major_tick_label_colours + Colours of the major tick labels. + minor_tick_labels + Whether to draw the minor tick labels. + minor_tick_label_colours + Colours of the minor tick labels. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualGrid() + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualGrid.png + :align: center + :alt: visual-grid + """ + + def __init__( + self, + size: int = 20, + major_grid_colours: ArrayLike = np.array([0.5, 0.5, 0.5]), + minor_grid_colours: ArrayLike = np.array([0.25, 0.25, 0.25]), + major_tick_labels=True, + major_tick_label_colours: ArrayLike = np.array([0.75, 0.75, 0.75]), + minor_tick_labels=True, + minor_tick_label_colours: ArrayLike = np.array([0.5, 0.5, 0.5]), + ): + super().__init__() + + size = int(size) + + vertices, faces, outline = conform_primitive_dtype( + primitive_grid( + width_segments=size, + height_segments=size, + ) + ) + positions = vertices["position"] + + self._grid_major = gfx.Mesh( + gfx.Geometry( + positions=as_contiguous_array(positions), + indices=outline[..., 1].reshape([-1, 4]), + colors=as_contiguous_array( + append_channel( + np.tile(major_grid_colours, (positions.shape[0], 1)), 1 + ) + ), + ), + gfx.MeshBasicMaterial(color_mode="vertex", wireframe=True), + ) + self._grid_major.local.scale = np.array([size, size, 1]) + self.add(self._grid_major) + + vertices, faces, outline = conform_primitive_dtype( + primitive_grid( + width_segments=size * 10, + height_segments=size * 10, + ) + ) + positions = vertices["position"] + + self._grid_minor = gfx.Mesh( + gfx.Geometry( + positions=as_contiguous_array(positions), + indices=outline[..., 1].reshape([-1, 4]), + colors=as_contiguous_array( + append_channel( + np.tile(minor_grid_colours, (positions.shape[0], 1)), 1 + ) + ), + ), + gfx.MeshBasicMaterial(color_mode="vertex", wireframe=True), + ) + self._grid_minor.local.position = np.array([0, 0, -1e-3]) + self._grid_minor.local.scale = np.array([size, size, 1]) + self.add(self._grid_minor) + + axes_positions = np.array( + [ + [0, 0, 0], + [1, 0, 0], + [0, 0, 0], + [0, 1, 0], + ], + dtype=DEFAULT_FLOAT_DTYPE_WGPU, + ) + axes_positions *= size / 2 + + axes_colours = np.array( + [ + [1, 0, 0, 1], + [1, 0, 0, 1], + [0, 1, 0, 1], + [0, 1, 0, 1], + ], + dtype=DEFAULT_FLOAT_DTYPE_WGPU, + ) + + self._axes_helper = gfx.Line( + gfx.Geometry(positions=axes_positions, colors=axes_colours), + gfx.LineSegmentMaterial(color_mode="vertex", thickness=2), + ) + self.add(self._axes_helper) + + if major_tick_labels: + self._ticks_major_x, self._ticks_major_y = [], [] + for i in np.arange(-size // 2, size // 2 + 1, 1): + x_text = gfx.Text( + gfx.TextGeometry( + f"{i} " if i == 0 else str(i), + font_size=CONSTANTS_COLOUR_STYLE.font_size.medium, + screen_space=True, + anchor="Top-Right" if i == 0 else "Top-Center", + ), + gfx.TextMaterial( + color=major_tick_label_colours # pyright: ignore + ), + ) + x_text.local.position = np.array([i, 0, 1e-3]) + self.add(x_text) + self._ticks_major_x.append(x_text) + + if i == 0: + continue + + y_text = gfx.Text( + gfx.TextGeometry( + f"{i} ", + font_size=CONSTANTS_COLOUR_STYLE.font_size.medium, + screen_space=True, + anchor="Center-Right", + ), + gfx.TextMaterial( + color=major_tick_label_colours # pyright: ignore + ), + ) + y_text.local.position = np.array([0, i, 1e-3]) + self.add(y_text) + self._ticks_major_y.append(y_text) + + if minor_tick_labels: + self._ticks_minor_x, self._ticks_minor_y = [], [] + for i in np.arange(-size // 2, size // 2 + 0.1, 0.1): + if np.around(i, 0) == np.around(i, 1): + continue + + i = np.around(i, 1) # noqa: PLW2901 + + x_text = gfx.Text( + gfx.TextGeometry( + f"{i} " if i == 0 else str(i), + font_size=CONSTANTS_COLOUR_STYLE.font_size.small, + screen_space=True, + anchor="Top-Right" if i == 0 else "Top-Center", + ), + gfx.TextMaterial( + color=minor_tick_label_colours # pyright: ignore + ), + ) + x_text.local.position = np.array([i, 0, 1e-3]) + self.add(x_text) + self._ticks_minor_x.append(x_text) + + if i == 0: + continue + + y_text = gfx.Text( + gfx.TextGeometry( + f"{i} ", + font_size=CONSTANTS_COLOUR_STYLE.font_size.small, + screen_space=True, + anchor="Center-Right", + ), + gfx.TextMaterial( + color=minor_tick_label_colours # pyright: ignore + ), + ) + y_text.local.position = np.array([0, i, 1e-3]) + self.add(y_text) + self._ticks_minor_y.append(y_text) + + +if __name__ == "__main__": + scene = gfx.Scene() + + scene.add( + gfx.Background( + None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ) + ) + + visual_1 = VisualGrid() + scene.add(visual_1) + + gfx.show(scene, up=np.array([0, 0, 1])) diff --git a/colour_visuals/pointer_gamut.py b/colour_visuals/pointer_gamut.py new file mode 100644 index 0000000..192280d --- /dev/null +++ b/colour_visuals/pointer_gamut.py @@ -0,0 +1,276 @@ +# !/usr/bin/env python +""" +Pointer Gamut Visuals +===================== + +Defines the *Pointer's Gamut* visuals: + +- :class:`colour_visuals.VisualPointerGamut2D` +""" + +from __future__ import annotations + +import numpy as np +import pygfx as gfx +from colour.hints import ArrayLike, Literal, LiteralColourspaceModel +from colour.models import ( + CCS_ILLUMINANT_POINTER_GAMUT, + DATA_POINTER_GAMUT_VOLUME, + Lab_to_XYZ, + LCHab_to_Lab, +) +from colour.plotting import ( + CONSTANTS_COLOUR_STYLE, + XYZ_to_plotting_colourspace, + colourspace_model_axis_reorder, + lines_pointer_gamut, +) + +from colour_visuals.common import ( + DEFAULT_FLOAT_DTYPE_WGPU, + XYZ_to_colourspace_model, + append_channel, + as_contiguous_array, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = ["VisualPointerGamut2D", "VisualPointerGamut3D"] + + +class VisualPointerGamut2D(gfx.Group): + """ + Create a 2D *Pointer's Gamut* visual. + + Parameters + ---------- + method + *Chromaticity Diagram* method. + colours + Colours of the visual, if *None*, the colours are computed from the + visual geometry. + opacity + Opacity of the visual. + thickness + Thickness of the visual lines. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualPointerGamut2D() + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualPointerGamut2D.png + :align: center + :alt: visual-pointer-gamut-2-d + """ + + def __init__( + self, + method: Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] + | str = "CIE 1931", + colours: ArrayLike | None = None, + opacity: float = 1, + thickness: float = 1, + ): + super().__init__() + + lines_b, lines_v = lines_pointer_gamut(method) + + # Boundary + positions = np.concatenate( + [lines_b["position"][:-1], lines_b["position"][1:]], axis=1 + ).reshape([-1, 2]) + positions = np.hstack( + [ + positions, + np.full((positions.shape[0], 1), 0, DEFAULT_FLOAT_DTYPE_WGPU), + ] + ) + + if colours is None: + colours_b = np.concatenate( + [lines_b["colour"][:-1], lines_b["colour"][1:]], axis=1 + ).reshape([-1, 3]) + else: + colours_b = np.tile(colours, (positions.shape[0], 1)) + + self._pointer_gamut_boundary = gfx.Line( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array(append_channel(colours_b, opacity)), + ), + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + self.add(self._pointer_gamut_boundary) + + # Volume + positions = np.hstack( + [ + lines_v["position"], + np.full( + (lines_v["position"].shape[0], 1), + 0, + DEFAULT_FLOAT_DTYPE_WGPU, + ), + ] + ) + + if colours is None: + colours_v = lines_v["colour"] + else: + colours_v = np.tile(colours, (lines_v["colour"].shape[0], 1)) + + self._pointer_gamut_volume = gfx.Points( + gfx.Geometry( + positions=as_contiguous_array(positions), + sizes=as_contiguous_array( + np.full(positions.shape[0], thickness * 3) + ), + colors=as_contiguous_array(append_channel(colours_v, opacity)), + ), + gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), + ) + self.add(self._pointer_gamut_volume) + + +class VisualPointerGamut3D(gfx.Line): + """ + Create a 3D *Pointer's Gamut* visual. + + Parameters + ---------- + model + Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for + the list of supported colourspace models. + colours + Colours of the visual, if *None*, the colours are computed from the + visual geometry. + opacity + Opacity of the visual. + thickness + Thickness of the visual lines. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualPointerGamut3D() + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualPointerGamut3D.png + :align: center + :alt: visual-pointer-gamut-3-d + """ + + def __init__( + self, + model: LiteralColourspaceModel | str = "CIE xyY", + colours: ArrayLike | None = None, + opacity: float = 0.5, + thickness: float = 1, + ): + super().__init__() + + illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint + + data_pointer_gamut = np.reshape( + Lab_to_XYZ( + LCHab_to_Lab(DATA_POINTER_GAMUT_VOLUME), + CCS_ILLUMINANT_POINTER_GAMUT, + ), + (16, -1, 3), + ) + + sections = [] + for i in range(16): + section = np.vstack( + [data_pointer_gamut[i], data_pointer_gamut[i][0, ...]] + ) + sections.append( + np.concatenate([section[:-1], section[1:]], axis=1).reshape( + [-1, 3] + ) + ) + + positions = colourspace_model_axis_reorder( + XYZ_to_colourspace_model( + sections, + CCS_ILLUMINANT_POINTER_GAMUT, + model, + ), + model, + ).reshape([-1, 3]) + + if colours is None: + colours = XYZ_to_plotting_colourspace( + sections, illuminant + ).reshape([-1, 3]) + else: + colours = np.tile(colours, (positions.shape[0], 1)) + + super().__init__( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array(append_channel(colours, opacity)), + ), + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + + +if __name__ == "__main__": + from colour_visuals.diagrams import VisualSpectralLocus2D + + scene = gfx.Scene() + + scene.add( + gfx.Background( + None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ) + ) + + visual_1 = VisualSpectralLocus2D() + scene.add(visual_1) + + visual_2 = VisualPointerGamut2D() + scene.add(visual_2) + + visual_3 = VisualPointerGamut2D(colours=np.array([0.5, 0.5, 0.5])) + visual_3.local.position = np.array([1, 0, 0]) + scene.add(visual_3) + + visual_4 = VisualPointerGamut3D() + visual_4.local.position = np.array([2, 0, 0]) + scene.add(visual_4) + + gfx.show(scene, up=np.array([0, 0, 1])) diff --git a/colour_visuals/py.typed b/colour_visuals/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/colour_visuals/rgb_colourspace.py b/colour_visuals/rgb_colourspace.py new file mode 100644 index 0000000..b0a20a9 --- /dev/null +++ b/colour_visuals/rgb_colourspace.py @@ -0,0 +1,335 @@ +# !/usr/bin/env python +""" +RGB Colourspace Visuals +======================= + +Defines the *RGB colourspace* visuals: + +- :class:`colour_visuals.VisualRGBColourspace2D` +- :class:`colour_visuals.VisualRGBColourspace3D` +""" + +from __future__ import annotations + +import numpy as np +import pygfx as gfx +from colour.constants import EPSILON +from colour.geometry import primitive_cube +from colour.hints import ( + ArrayLike, + Literal, + LiteralColourspaceModel, + LiteralRGBColourspace, + Sequence, + Type, + cast, +) +from colour.models import RGB_Colourspace, RGB_to_XYZ, XYZ_to_RGB, xy_to_XYZ +from colour.plotting import ( + CONSTANTS_COLOUR_STYLE, + METHODS_CHROMATICITY_DIAGRAM, + colourspace_model_axis_reorder, + filter_RGB_colourspaces, +) +from colour.utilities import first_item, full + +from colour_visuals.common import ( + XYZ_to_colourspace_model, + append_channel, + as_contiguous_array, + conform_primitive_dtype, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "VisualRGBColourspace2D", + "VisualRGBColourspace3D", +] + + +class VisualRGBColourspace2D(gfx.Group): + """ + Create a 2D *RGB* colourspace gamut visual. + + Parameters + ---------- + colourspace + *RGB* colourspace to plot the gamut of. ``colourspaces`` elements + can be of any type or form supported by the + :func:`colour.plotting.common.filter_RGB_colourspaces` definition. + method + *Chromaticity Diagram* method. + colours + Colours of the visual, if *None*, the colours are computed from the + visual geometry. + opacity + Opacity of the visual. + thickness + Thickness of the visual lines. + + Examples + -------- + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualRGBColourspace2D() + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualRGBColourspace2D.png + :align: center + :alt: visual-rgbcolourspace-2-d + """ + + def __init__( + self, + colourspace: RGB_Colourspace + | str + | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] = "sRGB", + method: Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] + | str = "CIE 1931", + colours: ArrayLike | None = None, + opacity: float = 1, + thickness: float = 1, + ): + super().__init__() + + colourspace = cast( + RGB_Colourspace, + first_item(filter_RGB_colourspaces(colourspace).values()), + ) + + plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace + + XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[method]["XYZ_to_ij"] + + ij = XYZ_to_ij( + xy_to_XYZ(colourspace.primaries), plotting_colourspace.whitepoint + ) + ij[np.isnan(ij)] = 0 + + positions = append_channel( + np.array([ij[0], ij[1], ij[1], ij[2], ij[2], ij[0]]), 0 + ) + + if colours is None: + RGB = XYZ_to_RGB( + xy_to_XYZ(colourspace.primaries), plotting_colourspace + ) + colours_g = np.array( + [RGB[0], RGB[1], RGB[1], RGB[2], RGB[2], RGB[0]] + ) + else: + colours_g = np.tile(colours, (positions.shape[0], 1)) + + self._gamut = gfx.Line( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array(append_channel(colours_g, opacity)), + ), + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + self.add(self._gamut) + + ij = XYZ_to_ij( + xy_to_XYZ(colourspace.whitepoint), plotting_colourspace.whitepoint + ) + + positions = append_channel(ij, 0).reshape([-1, 3]) + + if colours is None: + colours_w = XYZ_to_RGB( + xy_to_XYZ(colourspace.whitepoint), plotting_colourspace + ).reshape([-1, 3]) + else: + colours_w = np.tile(colours, (positions.shape[0], 1)) + + self._whitepoint = gfx.Points( + gfx.Geometry( + positions=as_contiguous_array(positions), + sizes=as_contiguous_array( + full(positions.shape[0], thickness * 3) + ), + colors=as_contiguous_array(append_channel(colours_w, opacity)), + ), + gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), + ) + self.add(self._whitepoint) + + +class VisualRGBColourspace3D(gfx.Mesh): + """ + Create a 3D *RGB* colourspace volume visual. + + Parameters + ---------- + colourspace + *RGB* colourspace to plot the gamut of. ``colourspaces`` elements + can be of any type or form supported by the + :func:`colour.plotting.common.filter_RGB_colourspaces` definition. + model + Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for + the list of supported colourspace models. + colours + Colours of the visual, if *None*, the colours are computed from the + visual geometry. + opacity + Opacity of the visual. + thickness + Thickness of the visual lines. + material + Material used to surface the visual geomeetry. + wireframe + Whether to render the visual as a wireframe, i.e., only render edges. + segments + Edge segments count for the *RGB* colourspace cube. + + Examples + -------- + >>> import pylinalg as la + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualRGBColourspace3D(wireframe=True) + ... visual.local.rotation = la.quat_from_euler( + ... (-np.pi / 4, 0), order="XY" + ... ) + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualRGBColourspace3D.png + :align: center + :alt: visual-rgbcolourspace-3-d + """ + + def __init__( + self, + colourspace: RGB_Colourspace + | str + | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] = "sRGB", + model: LiteralColourspaceModel | str = "CIE xyY", + colours: ArrayLike | None = None, + opacity: float = 1, + material: Type[gfx.MeshAbstractMaterial] = gfx.MeshBasicMaterial, + wireframe: bool = False, + segments: int = 16, + ): + colourspace = cast( + RGB_Colourspace, + first_item(filter_RGB_colourspaces(colourspace).values()), + ) + + vertices, faces, outline = conform_primitive_dtype( + primitive_cube( + width_segments=segments, + height_segments=segments, + depth_segments=segments, + ) + ) + + positions = vertices["position"] + 0.5 + + positions[positions == 0] = EPSILON + + if colours is None: + colours = positions + else: + colours = np.tile(colours, (positions.shape[0], 1)) + + positions = colourspace_model_axis_reorder( + XYZ_to_colourspace_model( + RGB_to_XYZ(positions, colourspace), + colourspace.whitepoint, + model, + ), + model, + ) + + super().__init__( + gfx.Geometry( + positions=as_contiguous_array(positions), + normals=vertices["normal"], + indices=outline[..., 1].reshape([-1, 4]), + colors=as_contiguous_array(append_channel(colours, opacity)), + ), + material(color_mode="vertex", wireframe=wireframe) + if wireframe + else material(color_mode="vertex"), + ) + + +if __name__ == "__main__": + scene = gfx.Scene() + + scene.add( + gfx.Background( + None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ) + ) + + light_1 = gfx.AmbientLight() + scene.add(light_1) + + light_2 = gfx.DirectionalLight() + light_2.local.position = np.array([1, 1, 0]) + scene.add(light_2) + + visual_1 = VisualRGBColourspace3D() + scene.add(visual_1) + + visual_2 = VisualRGBColourspace3D(wireframe=True) + visual_2.local.position = np.array([0.5, 0, 0]) + scene.add(visual_2) + + visual_3 = VisualRGBColourspace3D(material=gfx.MeshNormalMaterial) + visual_3.local.position = np.array([1, 0, 0]) + scene.add(visual_3) + + visual_4 = VisualRGBColourspace3D( + model="CIE Lab", + colours=np.array([0.5, 0.5, 0.5]), + opacity=1, + material=gfx.MeshStandardMaterial, + ) + visual_4.local.position = np.array([2.5, 0, 0]) + scene.add(visual_4) + + visual_5 = VisualRGBColourspace2D() + visual_5.local.position = np.array([3.5, 0, 0]) + scene.add(visual_5) + + visual_6 = VisualRGBColourspace2D( + method="CIE 1976 UCS", + colours=np.array([0.5, 0.5, 0.5]), + opacity=1, + ) + visual_6.local.position = np.array([4.5, 0, 0]) + scene.add(visual_6) + + gfx.show(scene, up=np.array([0, 0, 1])) diff --git a/colour_visuals/rgb_scatter.py b/colour_visuals/rgb_scatter.py new file mode 100644 index 0000000..b4fe76a --- /dev/null +++ b/colour_visuals/rgb_scatter.py @@ -0,0 +1,163 @@ +# !/usr/bin/env python +""" +RGB Scatter Visuals +=================== + +Defines the *RGB* scatter visuals: + +- :class:`colour_visuals.VisualRGBScatter3D` +""" + +from __future__ import annotations + +import numpy as np +import pygfx as gfx +from colour import RGB_to_XYZ +from colour.constants import EPSILON +from colour.hints import ( + ArrayLike, + LiteralColourspaceModel, + LiteralRGBColourspace, + Sequence, + cast, +) +from colour.models import RGB_Colourspace +from colour.plotting import ( + colourspace_model_axis_reorder, + filter_RGB_colourspaces, +) +from colour.utilities import as_float_array, first_item + +from colour_visuals.common import ( + XYZ_to_colourspace_model, + append_channel, + as_contiguous_array, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = ["VisualRGBScatter3D"] + + +class VisualRGBScatter3D(gfx.Points): + """ + Create a 3D *RGB* scatter visual. + + Parameters + ---------- + RGB + *RGB* colourspace array. + colourspace + *RGB* colourspace of the *RGB* array. ``colourspace`` can be of any + type or form supported by the + :func:`colour.plotting.common.filter_RGB_colourspaces` definition. + model + Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for + the list of supported colourspace models. + colours + Colours of the visual, if *None*, the colours are computed from the + visual geometry. + opacity + Opacity of the visual. + size + Size of the visual points + + Examples + -------- + >>> import pylinalg as la + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualRGBScatter3D(np.random.random([24, 32, 3])) + ... visual.local.rotation = la.quat_from_euler( + ... (-np.pi / 4, 0), order="XY" + ... ) + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualRGBScatter3D.png + :align: center + :alt: visual-rgbscatter-3-d + """ + + def __init__( + self, + RGB: ArrayLike, + colourspace: RGB_Colourspace + | str + | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] = "sRGB", + model: LiteralColourspaceModel | str = "CIE xyY", + colours: ArrayLike | None = None, + opacity: float = 1, + size: float = 2, + ): + colourspace = cast( + RGB_Colourspace, + first_item(filter_RGB_colourspaces(colourspace).values()), + ) + + RGB = as_float_array(RGB).reshape(-1, 3) + + RGB[RGB == 0] = EPSILON + + XYZ = RGB_to_XYZ(RGB, colourspace) + + positions = colourspace_model_axis_reorder( + XYZ_to_colourspace_model(XYZ, colourspace.whitepoint, model), + model, + ) + + if colours is None: # noqa: SIM108 + colours = RGB + else: + colours = np.tile(colours, (RGB.shape[0], 1)) + + super().__init__( + gfx.Geometry( + positions=as_contiguous_array(positions), + sizes=as_contiguous_array(np.full(positions.shape[0], size)), + colors=as_contiguous_array(append_channel(colours, opacity)), + ), + gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), + ) + + +if __name__ == "__main__": + from colour_visuals.rgb_colourspace import VisualRGBColourspace3D + + scene = gfx.Scene() + + scene.add( + gfx.Background( + None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ) + ) + + visual_1 = VisualRGBScatter3D(np.random.random((64, 64, 3))) + scene.add(visual_1) + + visual_2 = VisualRGBColourspace3D(segments=8, wireframe=True) + scene.add(visual_2) + + visual_3 = VisualRGBScatter3D( + np.random.random((64, 64, 3)), colours=np.array([0.5, 0.5, 0.5]) + ) + visual_3.local.position = np.array([0.5, 0, 0]) + scene.add(visual_3) + + gfx.show(scene, up=np.array([0, 0, 1])) diff --git a/colour_visuals/rosch_macadam.py b/colour_visuals/rosch_macadam.py new file mode 100644 index 0000000..9c8528b --- /dev/null +++ b/colour_visuals/rosch_macadam.py @@ -0,0 +1,197 @@ +# !/usr/bin/env python +""" +Rösch-MacAdam Visuals +===================== + +Defines the *Rösch-MacAdam* visuals: + +- :class:`colour_visuals.VisualRoschMacAdam` +""" + +from __future__ import annotations + +import numpy as np +import pygfx as gfx +from colour.colorimetry import ( + MultiSpectralDistributions, + SpectralDistribution, + SpectralShape, +) +from colour.constants import EPSILON +from colour.hints import ArrayLike, LiteralColourspaceModel, Sequence, cast +from colour.models import XYZ_to_RGB +from colour.plotting import ( + CONSTANTS_COLOUR_STYLE, + colourspace_model_axis_reorder, + filter_cmfs, + filter_illuminants, +) +from colour.utilities import ( + first_item, +) +from colour.volume import XYZ_outer_surface + +from colour_visuals.common import ( + XYZ_to_colourspace_model, + append_channel, + as_contiguous_array, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "VisualRoschMacAdam", +] + + +class VisualRoschMacAdam(gfx.Line): + """ + Create a *Rösch-MacAdam* visual. + + Parameters + ---------- + cmfs + Standard observer colour matching functions, default to the + *CIE 1931 2 Degree Standard Observer*. ``cmfs`` can be of any type or + form supported by the :func:`colour.plotting.common.filter_cmfs` + definition. + illuminant + Illuminant spectral distribution, default to *CIE Illuminant E*. + ``illuminant`` can be of any type or form supported by the + :func:`colour.plotting.common.filter_illuminants` definition. + model + Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for + the list of supported colourspace models. + colours + Colours of the visual, if *None*, the colours are computed from the + visual geometry. + opacity + Opacity of the visual. + thickness + Thickness of the visual lines. + + Examples + -------- + >>> import pylinalg as la + >>> from colour.utilities import suppress_stdout + >>> from wgpu.gui.auto import WgpuCanvas + >>> with suppress_stdout(): + ... canvas = WgpuCanvas(size=(960, 540)) + ... scene = gfx.Scene() + ... scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) + ... visual = VisualRoschMacAdam() + ... visual.local.rotation = la.quat_from_euler( + ... (-np.pi / 4, 0), order="XY" + ... ) + ... camera = gfx.PerspectiveCamera(50, 16 / 9) + ... camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + ... scene.add(visual) + ... gfx.show(scene, camera=camera, canvas=canvas) + ... + + .. image:: ../_static/Plotting_VisualRoschMacAdam.png + :align: center + :alt: visual-rosch-mac-adam + """ + + def __init__( + self, + cmfs: MultiSpectralDistributions + | str + | Sequence[ + MultiSpectralDistributions | str + ] = "CIE 1931 2 Degree Standard Observer", + illuminant: SpectralDistribution + | str + | Sequence[SpectralDistribution | str] = "E", + model: LiteralColourspaceModel | str = "CIE xyY", + colours: ArrayLike | None = None, + opacity: float = 1, + thickness: float = 1, + ): + super().__init__() + + cmfs = cast( + MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values()) + ) + illuminant = cast( + SpectralDistribution, + first_item(filter_illuminants(illuminant).values()), + ) + + colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace + + XYZ = XYZ_outer_surface( + cmfs.copy().align( + SpectralShape(cmfs.shape.start, cmfs.shape.end, 5) + ), + illuminant, + ) + + XYZ[XYZ == 0] = EPSILON + + positions = colourspace_model_axis_reorder( + XYZ_to_colourspace_model(XYZ, colourspace.whitepoint, model), + model, + ) + positions = np.concatenate( + [positions[:-1], positions[1:]], axis=1 + ).reshape([-1, 3]) + + if colours is None: + colours = XYZ_to_RGB(XYZ, colourspace) + colours = np.concatenate( + [colours[:-1], colours[1:]], axis=1 + ).reshape([-1, 3]) + else: + colours = np.tile(colours, (positions.shape[0], 1)) + + super().__init__( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array(append_channel(colours, opacity)), + ), + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + + +if __name__ == "__main__": + scene = gfx.Scene() + + scene.add( + gfx.Background( + None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ) + ) + + visual_1 = VisualRoschMacAdam() + scene.add(visual_1) + + visual_2 = VisualRoschMacAdam( + model="CIE XYZ", colours=np.array([0.5, 0.5, 0.5]) + ) + visual_2.local.position = np.array([1, 0, 0]) + scene.add(visual_2) + + visual_3 = VisualRoschMacAdam( + model="JzAzBz", colours=np.array([0.5, 0.5, 0.5]) + ) + visual_3.local.position = np.array([3.5, 0, 0]) + scene.add(visual_3) + + visual_4 = VisualRoschMacAdam( + model="ICtCp", colours=np.array([0.5, 0.5, 0.5]) + ) + visual_4.local.position = np.array([6, 0, 0]) + scene.add(visual_4) + + gfx.show(scene, up=np.array([0, 0, 1])) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..8ca604b --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/colour_visuals.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/colour_visuals.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/colour_visuals" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/colour_visuals" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_static/Logo_Dark_001.svg b/docs/_static/Logo_Dark_001.svg new file mode 100644 index 0000000..5213139 --- /dev/null +++ b/docs/_static/Logo_Dark_001.svg @@ -0,0 +1,59 @@ + + + + + + + + + diff --git a/docs/_static/Logo_Light_001.svg b/docs/_static/Logo_Light_001.svg new file mode 100644 index 0000000..a58355f --- /dev/null +++ b/docs/_static/Logo_Light_001.svg @@ -0,0 +1,59 @@ + + + + + + + + + diff --git a/docs/_static/Logo_Medium_001.png b/docs/_static/Logo_Medium_001.png new file mode 100644 index 0000000..9bdd6d6 Binary files /dev/null and b/docs/_static/Logo_Medium_001.png differ diff --git a/docs/_templates/class.rst b/docs/_templates/class.rst new file mode 100644 index 0000000..07e2fd4 --- /dev/null +++ b/docs/_templates/class.rst @@ -0,0 +1,8 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :special-members: + :show-inheritance: diff --git a/docs/colour_visuals.rst b/docs/colour_visuals.rst new file mode 100644 index 0000000..44422e5 --- /dev/null +++ b/docs/colour_visuals.rst @@ -0,0 +1,106 @@ +Colour - Visuals +================ + +Chromaticity Diagram Visuals +---------------------------- + +``colour_visuals`` + +.. currentmodule:: colour_visuals + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + VisualSpectralLocus2D + VisualSpectralLocus3D + VisualChromaticityDiagram + VisualChromaticityDiagramCIE1931 + VisualChromaticityDiagramCIE1960UCS + VisualChromaticityDiagramCIE1976UCS + +Grid Visuals +------------ + +``colour_visuals`` + +.. currentmodule:: colour_visuals + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + VisualGrid + +Pointer's Gamut Visuals +----------------------- + +``colour_visuals`` + +.. currentmodule:: colour_visuals + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + VisualPointerGamut2D + VisualPointerGamut3D + +RGB Colourspace Visuals +----------------------- + +``colour_visuals`` + +.. currentmodule:: colour_visuals + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + VisualRGBColourspace2D + VisualRGBColourspace3D + +RGB Scatter Visuals +------------------- + +``colour_visuals`` + +.. currentmodule:: colour_visuals + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + VisualRGBScatter3D + +Rösch-MacAdam Visuals +--------------------- + +``colour_visuals`` + +.. currentmodule:: colour_visuals + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + VisualRoschMacAdam + + +Common Utilities +---------------- + +``colour_visuals.common`` + +.. currentmodule:: colour_visuals.common + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + DEFAULT_FLOAT_DTYPE_WGPU + DEFAULT_INT_DTYPE_WGPU + XYZ_to_colourspace_model + as_contiguous_array + conform_primitive_dtype + append_channel \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..b82acc2 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,189 @@ +""" +Colour - Visuals - Documentation Configuration +============================================== +""" + +import re +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +import colour_visuals as package # noqa: E402 + +basename = re.sub( + "_(\\w)", lambda x: x.group(1).upper(), package.__name__.title() +) + +# -- General configuration ------------------------------------------------ +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.ifconfig", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.viewcode", +] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "colour-science": ("https://colour.readthedocs.io/en/stable/", None), + "matplotlib": ("https://matplotlib.org/stable", None), + "numpy": ("https://numpy.org/doc/stable", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), +} + +autodoc_member_order = "bysource" +autodoc_mock_imports = [ + "colour", + "scipy", + "scipy.ndimage.filters", +] +autodoc_typehints = "both" +autodoc_type_aliases = { + "ArrayLike": "ArrayLike", + "DType": "DType", + "DTypeBoolean": "DTypeBoolean", + "DTypeComplex": "DTypeComplex", + "DTypeFloat": "DTypeFloat", + "DTypeInt": "DTypeInt", + "DTypeReal": "DTypeReal", + "Dataclass": "Dataclass", + "NDArrayBoolean": "NDArrayBoolean", + "NDArrayComplex": "NDArrayComplex", + "NDArrayFloat": "NDArrayFloat", + "NDArrayInt": "NDArrayInt", + "NDArrayReal": "NDArrayReal", + "NDArrayStr": "NDArrayStr", + "Real": "Real", +} +autodoc_preserve_defaults = True + +autoclass_content = "both" + +autosummary_generate = True + +bibtex_bibfiles = ["bibliography.bib"] +bibtex_encoding = "utf8" + +napoleon_custom_sections = ["Attributes", "Methods"] + +templates_path = ["_templates"] +source_suffix = ".rst" +master_doc = "index" + +project = package.__application_name__ +copyright = package.__copyright__.replace("Copyright (C)", "") # noqa: A001 +version = f"{package.__major_version__}.{package.__minor_version__}" +release = package.__version__ + +exclude_patterns = ["_build"] + +pygments_style = "lovelace" + +# -- Options for HTML output ---------------------------------------------- +html_theme = "pydata_sphinx_theme" +html_theme_options = { + "show_nav_level": 2, + "icon_links": [ + { + "name": "Email", + "url": "mailto:colour-developers@colour-science.org", + "icon": "fas fa-envelope", + }, + { + "name": "GitHub", + "url": ( + f"https://github.com/colour-science/" + f"{package.__name__.replace('_', '-')}" + ), + "icon": "fab fa-github", + }, + { + "name": "Facebook", + "url": "https://www.facebook.com/python.colour.science", + "icon": "fab fa-facebook", + }, + { + "name": "Gitter", + "url": "https://gitter.im/colour-science/colour", + "icon": "fab fa-gitter", + }, + { + "name": "Twitter", + "url": "https://twitter.com/colour_science", + "icon": "fab fa-twitter", + }, + ], +} +html_logo = "_static/Logo_Light_001.svg" +html_static_path = ["_static"] +htmlhelp_basename = f"{basename}Doc" + +# -- Options for LaTeX output --------------------------------------------- +latex_elements = { + "papersize": "a4paper", + "pointsize": "10pt", + "preamble": """ +\\usepackage{charter} +\\usepackage[defaultsans]{lato} +\\usepackage{inconsolata} + +% Ignoring unicode errors. +\\makeatletter +\\def\\UTFviii@defined#1{% + \\ifx#1\\relax + ?% + \\else\\expandafter + #1% + \\fi +} +\\makeatother + """, +} +latex_documents = [ + ( + "index", + f"{basename}.tex", + f"{package.__application_name__} Documentation", + package.__author__, + "manual", + ), +] +latex_logo = "_static/Logo_Medium_001.png" + +# -- Options for manual page output --------------------------------------- +man_pages = [ + ( + "index", + basename, + f"{package.__application_name__} Documentation", + [package.__author__], + 1, + ) +] + +# -- Options for Texinfo output ------------------------------------------- +texinfo_documents = [ + ( + "index", + basename, + f"{package.__application_name__} Documentation", + package.__author__, + package.__application_name__, + basename, + "Miscellaneous", + ), +] + +# -- Options for Epub output ---------------------------------------------- +epub_title = package.__application_name__ +epub_author = package.__author__ +epub_publisher = package.__author__ +epub_copyright = package.__copyright__.replace("Copyright (C)", "") +epub_exclude_files = ["search.html"] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..5dda663 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,68 @@ +Colour - Visuals +==================== + +A `Python `__ package implementing various +CFA (Colour Filter Array) visuals algorithms and related utilities. + +It is open source and freely available under the +`BSD-3-Clause `__ terms. + +.. image:: https://raw.githubusercontent.com/colour-science/colour-visuals/master/docs/_static/Visuals_001.png + +.. sectnum:: + +Features +-------- + +The following CFA (Colour Filter Array) visuals algorithms are implemented: + +- Bilinear +- Malvar (2004) +- DDFAPD - Menon (2007) + +Examples +^^^^^^^^ + +Various usage examples are available from the +`examples directory `__. + +User Guide +---------- + +.. toctree:: + :maxdepth: 2 + + user-guide + +API Reference +------------- + +.. toctree:: + :maxdepth: 2 + + reference + +Code of Conduct +--------------- + +The *Code of Conduct*, adapted from the `Contributor Covenant 1.4 `__, +is available on the `Code of Conduct `__ page. + +Contact & Social +---------------- + +The *Colour Developers* can be reached via different means: + +- `Email `__ +- `Facebook `__ +- `Github Discussions `__ +- `Gitter `__ +- `Twitter `__ + +About +----- + +| **Colour - Visuals** by Colour Developers +| Copyright 2023 Colour Developers – `colour-developers@colour-science.org `__ +| This software is released under terms of BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause +| `https://github.com/colour-science/colour-visuals `__ diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..96c47c9 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,8 @@ +Installation Guide +================== + +Primary Dependencies +-------------------- + +Pypi +---- diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..fd7b014 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\colour_visuals.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\colour_visuals.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 0000000..050b44b --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,13 @@ +API Reference +============= + +.. toctree:: + :titlesonly: + + colour_visuals + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`search` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..9cde8f7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,50 @@ +accessible-pygments==0.0.4 ; python_version >= "3.9" and python_version < "3.12" +alabaster==0.7.13 ; python_version >= "3.9" and python_version < "3.12" +babel==2.12.1 ; python_version >= "3.9" and python_version < "3.12" +beautifulsoup4==4.12.2 ; python_version >= "3.9" and python_version < "3.12" +biblib-simple==0.1.2 ; python_version >= "3.9" and python_version < "3.12" +certifi==2023.7.22 ; python_version >= "3.9" and python_version < "3.12" +charset-normalizer==3.2.0 ; python_version >= "3.9" and python_version < "3.12" +colorama==0.4.6 ; python_version >= "3.9" and python_version < "3.12" and sys_platform == "win32" +colour-science==0.4.3 ; python_version >= "3.9" and python_version < "3.12" +contourpy==1.1.0 ; python_version >= "3.9" and python_version < "3.12" +cycler==0.11.0 ; python_version >= "3.9" and python_version < "3.12" +docutils==0.17.1 ; python_version >= "3.9" and python_version < "3.12" +fonttools==4.42.1 ; python_version >= "3.9" and python_version < "3.12" +idna==3.4 ; python_version >= "3.9" and python_version < "3.12" +imageio==2.31.2 ; python_version >= "3.9" and python_version < "3.12" +imagesize==1.4.1 ; python_version >= "3.9" and python_version < "3.12" +importlib-metadata==6.8.0 ; python_version >= "3.9" and python_version < "3.10" +importlib-resources==6.0.1 ; python_version >= "3.9" and python_version < "3.10" +jinja2==3.1.2 ; python_version >= "3.9" and python_version < "3.12" +kiwisolver==1.4.5 ; python_version >= "3.9" and python_version < "3.12" +latexcodec==2.0.1 ; python_version >= "3.9" and python_version < "3.12" +markupsafe==2.1.3 ; python_version >= "3.9" and python_version < "3.12" +matplotlib==3.7.2 ; python_version >= "3.9" and python_version < "3.12" +numpy==1.25.2 ; python_version >= "3.9" and python_version < "3.12" +packaging==23.1 ; python_version >= "3.9" and python_version < "3.12" +pillow==10.0.0 ; python_version >= "3.9" and python_version < "3.12" +pybtex-docutils==1.0.3 ; python_version >= "3.9" and python_version < "3.12" +pybtex==0.24.0 ; python_version >= "3.9" and python_version < "3.12" +pydata-sphinx-theme==0.13.3 ; python_version >= "3.9" and python_version < "3.12" +pygments==2.16.1 ; python_version >= "3.9" and python_version < "3.12" +pyparsing==3.0.9 ; python_version >= "3.9" and python_version < "3.12" +python-dateutil==2.8.2 ; python_version >= "3.9" and python_version < "3.12" +pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "3.12" +requests==2.31.0 ; python_version >= "3.9" and python_version < "3.12" +restructuredtext-lint==1.4.0 ; python_version >= "3.9" and python_version < "3.12" +scipy==1.11.2 ; python_version >= "3.9" and python_version < "3.12" +six==1.16.0 ; python_version >= "3.9" and python_version < "3.12" +snowballstemmer==2.2.0 ; python_version >= "3.9" and python_version < "3.12" +soupsieve==2.4.1 ; python_version >= "3.9" and python_version < "3.12" +sphinx==4.5.0 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-applehelp==1.0.4 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-bibtex==2.6.0 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-htmlhelp==2.0.1 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.9" and python_version < "3.12" +typing-extensions==4.7.1 ; python_version >= "3.9" and python_version < "3.12" +urllib3==2.0.4 ; python_version >= "3.9" and python_version < "3.12" +zipp==3.16.2 ; python_version >= "3.9" and python_version < "3.10" diff --git a/docs/user-guide.rst b/docs/user-guide.rst new file mode 100644 index 0000000..54b9e29 --- /dev/null +++ b/docs/user-guide.rst @@ -0,0 +1,13 @@ +User Guide +========== + +The user guide provides an overview of **Colour - Visuals** and +explains important concepts and features, details can be found in the +`API Reference `__. + +.. toctree:: + :maxdepth: 1 + + Installation + Contributing + Changes diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..56abe4b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,202 @@ +[tool.poetry] +name = "colour-visuals" +version = "0.1.0" +description = "WebGPU-based visuals for colour science applications" +license = "BSD-3-Clause" +authors = [ "Colour Developers " ] +maintainers = [ "Colour Developers " ] +readme = 'README.rst' +repository = "https://github.com/colour-science/colour-visuals" +homepage = "https://www.colour-science.org/" +keywords = [ + "color", + "color-science", + "color-space", + "color-spaces", + "colorspace", + "colorspaces", + "colour", + "colour-science", + "colour-space", + "colour-spaces", + "colourspace", + "colourspaces", + "visuals", + "webgpu", + "python", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", + "Topic :: Software Development" +] + +[tool.poetry.dependencies] +python = ">= 3.9, < 3.12" +colour-science = {git = "https://github.com/colour-science/colour.git"} +imageio = ">= 2, < 3" +matplotlib = ">= 3.5, != 3.5.0, != 3.5.1" +networkx = ">= 2.7, < 3" +numpy = ">= 1.22, < 2" +pygfx = ">= 0.1, < 0.2" +pyside6 = ">= 6, < 7" +scipy = ">= 1.8, < 2" + +[tool.poetry.group.dev.dependencies] +black = "*" +blackdoc = "*" +coverage = "!= 6.3" +coveralls = "*" +flynt = "*" +invoke = "*" +jupyter = "*" +pre-commit = "*" +pyright = "*" +pytest = "*" +pytest-cov = "*" +pytest-xdist = "*" +ruff = "*" +toml = "*" +twine = "*" + +[tool.poetry.group.docs.dependencies] +pydata-sphinx-theme = "*" +restructuredtext-lint = "*" +sphinx = "*" + +[tool.black] +line-length = 79 +exclude = ''' +/( + \.git + | build + | dist +)/ +''' + +[tool.flynt] +line_length=999 + +[tool.isort] +ensure_newline_before_comments = true +force_grid_wrap = 0 +include_trailing_comma = true +line_length = 88 +multi_line_output = 3 +split_on_trailing_comma = true +use_parentheses = true + +[tool.pyright] +reportMissingImports = false +reportMissingModuleSource = false +reportUnboundVariable = false +reportUnnecessaryCast = true +reportUnnecessaryTypeIgnoreComment = true +reportUnsupportedDunderAll = false +reportUnusedExpression = false + +[tool.pytest.ini_options] +addopts = "-n auto --dist=loadscope --durations=5" + +[tool.ruff] +target-version = "py39" +line-length = 88 +select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + # "ANN", # flake8-annotations + "B", # flake8-bugbear + # "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + # "C90", # mccabe + # "COM", # flake8-commas + "DTZ", # flake8-datetimez + "D", # pydocstyle + "E", # pydocstyle + # "ERA", # eradicate + # "EM", # flake8-errmsg + "EXE", # flake8-executable + "F", # flake8 + # "FBT", # flake8-boolean-trap + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + # "PD", # pandas-vet + "PIE", # flake8-pie + "PGH", # pygrep-hooks + "PL", # pylint + # "PT", # flake8-pytest-style + # "PTH", # flake8-use-pathlib [Enable] + "Q", # flake8-quotes + "RET", # flake8-return + "RUF", # Ruff + "S", # flake8-bandit + "SIM", # flake8-simplify + "T10", # flake8-debugger + "T20", # flake8-print + # "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pydocstyle + "YTT" # flake8-2020 +] +ignore = [ + "B008", + "B905", + "D104", + "D200", + "D202", + "D205", + "D301", + "D400", + "I001", + "N801", + "N802", + "N803", + "N806", + "N813", + "N815", + "N816", + "PGH003", + "PIE804", + "PLE0605", + "PLR0911", + "PLR0912", + "PLR0913", + "PLR0915", + "PLR2004", + "RET504", + "RET505", + "RET506", + "RET507", + "RET508", + "TRY003", + "TRY300", +] +typing-modules = ["colour.hints"] +fixable = ["B", "C", "E", "F", "PIE", "RUF", "SIM", "UP", "W"] + +[tool.ruff.pydocstyle] +convention = "numpy" + +[tool.ruff.per-file-ignores] +"colour_visuals/examples/*" = ["INP", "T201", "T203"] +"docs/*" = ["INP"] +"tasks.py" = ["INP"] +"utilities/*" = ["EXE001", "INP"] +"utilities/unicode_to_ascii.py" = ["RUF001"] + +[build-system] +requires = [ "poetry_core>=1.0.0" ] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..edd4afd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,180 @@ +accessible-pygments==0.0.4 ; python_version >= "3.9" and python_version < "3.12" +alabaster==0.7.13 ; python_version >= "3.9" and python_version < "3.12" +anyio==3.7.1 ; python_version >= "3.9" and python_version < "3.12" +appnope==0.1.3 ; python_version >= "3.9" and python_version < "3.12" and (platform_system == "Darwin" or sys_platform == "darwin") +argon2-cffi-bindings==21.2.0 ; python_version >= "3.9" and python_version < "3.12" +argon2-cffi==23.1.0 ; python_version >= "3.9" and python_version < "3.12" +arrow==1.2.3 ; python_version >= "3.9" and python_version < "3.12" +astor==0.8.1 ; python_version >= "3.9" and python_version < "3.12" +asttokens==2.2.1 ; python_version >= "3.9" and python_version < "3.12" +async-lru==2.0.4 ; python_version >= "3.9" and python_version < "3.12" +attrs==23.1.0 ; python_version >= "3.9" and python_version < "3.12" +babel==2.12.1 ; python_version >= "3.9" and python_version < "3.12" +backcall==0.2.0 ; python_version >= "3.9" and python_version < "3.12" +beautifulsoup4==4.12.2 ; python_version >= "3.9" and python_version < "3.12" +biblib-simple==0.1.2 ; python_version >= "3.9" and python_version < "3.12" +black==23.7.0 ; python_version >= "3.9" and python_version < "3.12" +blackdoc==0.3.8 ; python_version >= "3.9" and python_version < "3.12" +bleach==6.0.0 ; python_version >= "3.9" and python_version < "3.12" +certifi==2023.7.22 ; python_version >= "3.9" and python_version < "3.12" +cffi==1.15.1 ; python_version >= "3.9" and python_version < "3.12" +cfgv==3.4.0 ; python_version >= "3.9" and python_version < "3.12" +charset-normalizer==3.2.0 ; python_version >= "3.9" and python_version < "3.12" +click==8.1.7 ; python_version >= "3.9" and python_version < "3.12" +colorama==0.4.6 ; python_version >= "3.9" and python_version < "3.12" and (sys_platform == "win32" or platform_system == "Windows") +colour-science==0.4.3 ; python_version >= "3.9" and python_version < "3.12" +comm==0.1.4 ; python_version >= "3.9" and python_version < "3.12" +contourpy==1.1.0 ; python_version >= "3.9" and python_version < "3.12" +coverage==6.5.0 ; python_version >= "3.9" and python_version < "3.12" +coverage[toml]==6.5.0 ; python_version >= "3.9" and python_version < "3.12" +coveralls==3.3.1 ; python_version >= "3.9" and python_version < "3.12" +cryptography==41.0.3 ; python_version >= "3.9" and python_version < "3.12" and sys_platform == "linux" +cycler==0.11.0 ; python_version >= "3.9" and python_version < "3.12" +debugpy==1.6.7.post1 ; python_version >= "3.9" and python_version < "3.12" +decorator==5.1.1 ; python_version >= "3.9" and python_version < "3.12" +defusedxml==0.7.1 ; python_version >= "3.9" and python_version < "3.12" +distlib==0.3.7 ; python_version >= "3.9" and python_version < "3.12" +docopt==0.6.2 ; python_version >= "3.9" and python_version < "3.12" +docutils==0.17.1 ; python_version >= "3.9" and python_version < "3.12" +exceptiongroup==1.1.3 ; python_version >= "3.9" and python_version < "3.11" +execnet==2.0.2 ; python_version >= "3.9" and python_version < "3.12" +executing==1.2.0 ; python_version >= "3.9" and python_version < "3.12" +fastjsonschema==2.18.0 ; python_version >= "3.9" and python_version < "3.12" +filelock==3.12.2 ; python_version >= "3.9" and python_version < "3.12" +flynt==1.0.1 ; python_version >= "3.9" and python_version < "3.12" +fonttools==4.42.1 ; python_version >= "3.9" and python_version < "3.12" +fqdn==1.5.1 ; python_version >= "3.9" and python_version < "3.12" +identify==2.5.27 ; python_version >= "3.9" and python_version < "3.12" +idna==3.4 ; python_version >= "3.9" and python_version < "3.12" +imageio==2.31.2 ; python_version >= "3.9" and python_version < "3.12" +imagesize==1.4.1 ; python_version >= "3.9" and python_version < "3.12" +importlib-metadata==6.8.0 ; python_version >= "3.9" and python_version < "3.12" +importlib-resources==6.0.1 ; python_version >= "3.9" and python_version < "3.10" +iniconfig==2.0.0 ; python_version >= "3.9" and python_version < "3.12" +invoke==2.2.0 ; python_version >= "3.9" and python_version < "3.12" +ipykernel==6.25.1 ; python_version >= "3.9" and python_version < "3.12" +ipython-genutils==0.2.0 ; python_version >= "3.9" and python_version < "3.12" +ipython==8.14.0 ; python_version >= "3.9" and python_version < "3.12" +ipywidgets==8.1.0 ; python_version >= "3.9" and python_version < "3.12" +isoduration==20.11.0 ; python_version >= "3.9" and python_version < "3.12" +jaraco-classes==3.3.0 ; python_version >= "3.9" and python_version < "3.12" +jedi==0.19.0 ; python_version >= "3.9" and python_version < "3.12" +jeepney==0.8.0 ; python_version >= "3.9" and python_version < "3.12" and sys_platform == "linux" +jinja2==3.1.2 ; python_version >= "3.9" and python_version < "3.12" +json5==0.9.14 ; python_version >= "3.9" and python_version < "3.12" +jsonpointer==2.4 ; python_version >= "3.9" and python_version < "3.12" +jsonschema-specifications==2023.7.1 ; python_version >= "3.9" and python_version < "3.12" +jsonschema==4.19.0 ; python_version >= "3.9" and python_version < "3.12" +jsonschema[format-nongpl]==4.19.0 ; python_version >= "3.9" and python_version < "3.12" +jupyter-client==8.3.0 ; python_version >= "3.9" and python_version < "3.12" +jupyter-console==6.6.3 ; python_version >= "3.9" and python_version < "3.12" +jupyter-core==5.3.1 ; python_version >= "3.9" and python_version < "3.12" +jupyter-events==0.7.0 ; python_version >= "3.9" and python_version < "3.12" +jupyter-lsp==2.2.0 ; python_version >= "3.9" and python_version < "3.12" +jupyter-server-terminals==0.4.4 ; python_version >= "3.9" and python_version < "3.12" +jupyter-server==2.7.2 ; python_version >= "3.9" and python_version < "3.12" +jupyter==1.0.0 ; python_version >= "3.9" and python_version < "3.12" +jupyterlab-pygments==0.2.2 ; python_version >= "3.9" and python_version < "3.12" +jupyterlab-server==2.24.0 ; python_version >= "3.9" and python_version < "3.12" +jupyterlab-widgets==3.0.8 ; python_version >= "3.9" and python_version < "3.12" +jupyterlab==4.0.5 ; python_version >= "3.9" and python_version < "3.12" +keyring==24.2.0 ; python_version >= "3.9" and python_version < "3.12" +kiwisolver==1.4.5 ; python_version >= "3.9" and python_version < "3.12" +latexcodec==2.0.1 ; python_version >= "3.9" and python_version < "3.12" +markdown-it-py==3.0.0 ; python_version >= "3.9" and python_version < "3.12" +markupsafe==2.1.3 ; python_version >= "3.9" and python_version < "3.12" +matplotlib-inline==0.1.6 ; python_version >= "3.9" and python_version < "3.12" +matplotlib==3.7.2 ; python_version >= "3.9" and python_version < "3.12" +mdurl==0.1.2 ; python_version >= "3.9" and python_version < "3.12" +mistune==3.0.1 ; python_version >= "3.9" and python_version < "3.12" +more-itertools==10.1.0 ; python_version >= "3.9" and python_version < "3.12" +mypy-extensions==1.0.0 ; python_version >= "3.9" and python_version < "3.12" +nbclient==0.8.0 ; python_version >= "3.9" and python_version < "3.12" +nbconvert==7.7.4 ; python_version >= "3.9" and python_version < "3.12" +nbformat==5.9.2 ; python_version >= "3.9" and python_version < "3.12" +nest-asyncio==1.5.7 ; python_version >= "3.9" and python_version < "3.12" +nodeenv==1.8.0 ; python_version >= "3.9" and python_version < "3.12" +notebook-shim==0.2.3 ; python_version >= "3.9" and python_version < "3.12" +notebook==7.0.2 ; python_version >= "3.9" and python_version < "3.12" +numpy==1.25.2 ; python_version >= "3.9" and python_version < "3.12" +overrides==7.4.0 ; python_version >= "3.9" and python_version < "3.12" +packaging==23.1 ; python_version >= "3.9" and python_version < "3.12" +pandocfilters==1.5.0 ; python_version >= "3.9" and python_version < "3.12" +parso==0.8.3 ; python_version >= "3.9" and python_version < "3.12" +pathspec==0.11.2 ; python_version >= "3.9" and python_version < "3.12" +pexpect==4.8.0 ; python_version >= "3.9" and python_version < "3.12" and sys_platform != "win32" +pickleshare==0.7.5 ; python_version >= "3.9" and python_version < "3.12" +pillow==10.0.0 ; python_version >= "3.9" and python_version < "3.12" +pkginfo==1.9.6 ; python_version >= "3.9" and python_version < "3.12" +platformdirs==3.10.0 ; python_version >= "3.9" and python_version < "3.12" +pluggy==1.3.0 ; python_version >= "3.9" and python_version < "3.12" +pre-commit==3.3.3 ; python_version >= "3.9" and python_version < "3.12" +prometheus-client==0.17.1 ; python_version >= "3.9" and python_version < "3.12" +prompt-toolkit==3.0.39 ; python_version >= "3.9" and python_version < "3.12" +psutil==5.9.5 ; python_version >= "3.9" and python_version < "3.12" +ptyprocess==0.7.0 ; python_version >= "3.9" and python_version < "3.12" and (sys_platform != "win32" or os_name != "nt") +pure-eval==0.2.2 ; python_version >= "3.9" and python_version < "3.12" +pybtex-docutils==1.0.3 ; python_version >= "3.9" and python_version < "3.12" +pybtex==0.24.0 ; python_version >= "3.9" and python_version < "3.12" +pycparser==2.21 ; python_version >= "3.9" and python_version < "3.12" +pydata-sphinx-theme==0.13.3 ; python_version >= "3.9" and python_version < "3.12" +pygments==2.16.1 ; python_version >= "3.9" and python_version < "3.12" +pyparsing==3.0.9 ; python_version >= "3.9" and python_version < "3.12" +pyright==1.1.324 ; python_version >= "3.9" and python_version < "3.12" +pytest-cov==4.1.0 ; python_version >= "3.9" and python_version < "3.12" +pytest-xdist==3.3.1 ; python_version >= "3.9" and python_version < "3.12" +pytest==7.4.0 ; python_version >= "3.9" and python_version < "3.12" +python-dateutil==2.8.2 ; python_version >= "3.9" and python_version < "3.12" +python-json-logger==2.0.7 ; python_version >= "3.9" and python_version < "3.12" +pywin32-ctypes==0.2.2 ; python_version >= "3.9" and python_version < "3.12" and sys_platform == "win32" +pywin32==306 ; sys_platform == "win32" and platform_python_implementation != "PyPy" and python_version >= "3.9" and python_version < "3.12" +pywinpty==2.0.11 ; python_version >= "3.9" and python_version < "3.12" and os_name == "nt" +pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "3.12" +pyzmq==25.1.1 ; python_version >= "3.9" and python_version < "3.12" +qtconsole==5.4.3 ; python_version >= "3.9" and python_version < "3.12" +qtpy==2.3.1 ; python_version >= "3.9" and python_version < "3.12" +readme-renderer==41.0 ; python_version >= "3.9" and python_version < "3.12" +referencing==0.30.2 ; python_version >= "3.9" and python_version < "3.12" +requests-toolbelt==1.0.0 ; python_version >= "3.9" and python_version < "3.12" +requests==2.31.0 ; python_version >= "3.9" and python_version < "3.12" +restructuredtext-lint==1.4.0 ; python_version >= "3.9" and python_version < "3.12" +rfc3339-validator==0.1.4 ; python_version >= "3.9" and python_version < "3.12" +rfc3986-validator==0.1.1 ; python_version >= "3.9" and python_version < "3.12" +rfc3986==2.0.0 ; python_version >= "3.9" and python_version < "3.12" +rich==13.5.2 ; python_version >= "3.9" and python_version < "3.12" +rpds-py==0.9.2 ; python_version >= "3.9" and python_version < "3.12" +ruff==0.0.286 ; python_version >= "3.9" and python_version < "3.12" +scipy==1.11.2 ; python_version >= "3.9" and python_version < "3.12" +secretstorage==3.3.3 ; python_version >= "3.9" and python_version < "3.12" and sys_platform == "linux" +send2trash==1.8.2 ; python_version >= "3.9" and python_version < "3.12" +setuptools==68.1.2 ; python_version >= "3.9" and python_version < "3.12" +six==1.16.0 ; python_version >= "3.9" and python_version < "3.12" +sniffio==1.3.0 ; python_version >= "3.9" and python_version < "3.12" +snowballstemmer==2.2.0 ; python_version >= "3.9" and python_version < "3.12" +soupsieve==2.4.1 ; python_version >= "3.9" and python_version < "3.12" +sphinx==4.5.0 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-applehelp==1.0.4 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-bibtex==2.6.0 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-htmlhelp==2.0.1 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.9" and python_version < "3.12" +sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.9" and python_version < "3.12" +stack-data==0.6.2 ; python_version >= "3.9" and python_version < "3.12" +terminado==0.17.1 ; python_version >= "3.9" and python_version < "3.12" +tinycss2==1.2.1 ; python_version >= "3.9" and python_version < "3.12" +toml==0.10.2 ; python_version >= "3.9" and python_version < "3.12" +tomli==2.0.1 ; python_version >= "3.9" and python_version < "3.12" +tornado==6.3.3 ; python_version >= "3.9" and python_version < "3.12" +traitlets==5.9.0 ; python_version >= "3.9" and python_version < "3.12" +twine==4.0.2 ; python_version >= "3.9" and python_version < "3.12" +typing-extensions==4.7.1 ; python_version >= "3.9" and python_version < "3.12" +uri-template==1.3.0 ; python_version >= "3.9" and python_version < "3.12" +urllib3==2.0.4 ; python_version >= "3.9" and python_version < "3.12" +virtualenv==20.24.3 ; python_version >= "3.9" and python_version < "3.12" +wcwidth==0.2.6 ; python_version >= "3.9" and python_version < "3.12" +webcolors==1.13 ; python_version >= "3.9" and python_version < "3.12" +webencodings==0.5.1 ; python_version >= "3.9" and python_version < "3.12" +websocket-client==1.6.2 ; python_version >= "3.9" and python_version < "3.12" +widgetsnbextension==4.0.8 ; python_version >= "3.9" and python_version < "3.12" +zipp==3.16.2 ; python_version >= "3.9" and python_version < "3.12" diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..0678a01 --- /dev/null +++ b/tasks.py @@ -0,0 +1,456 @@ +""" +Invoke - Tasks +============== +""" + +from __future__ import annotations + +import fnmatch +import inspect +import os +import re +import uuid + +from colour.utilities import message_box + +import colour_visuals + +if not hasattr(inspect, "getargspec"): + inspect.getargspec = inspect.getfullargspec # pyright: ignore + +from invoke.context import Context +from invoke.tasks import task + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "APPLICATION_NAME", + "APPLICATION_VERSION", + "PYTHON_PACKAGE_NAME", + "PYPI_PACKAGE_NAME", + "PYPI_ARCHIVE_NAME", + "BIBLIOGRAPHY_NAME", + "clean", + "formatting", + "quality", + "precommit", + "tests", + "examples", + "preflight", + "docs", + "todo", + "requirements", + "build", + "virtualise", + "tag", + "release", + "sha256", +] + +APPLICATION_NAME: str = colour_visuals.__application_name__ + +APPLICATION_VERSION: str = colour_visuals.__version__ + +PYTHON_PACKAGE_NAME: str = colour_visuals.__name__ + +PYPI_PACKAGE_NAME: str = "colour-visuals" +PYPI_ARCHIVE_NAME: str = PYPI_PACKAGE_NAME.replace("-", "_") + +BIBLIOGRAPHY_NAME: str = "BIBLIOGRAPHY.bib" + + +@task +def clean( + ctx: Context, + docs: bool = True, + bytecode: bool = False, + pytest: bool = True, +): + """ + Clean the project. + + Parameters + ---------- + ctx + Context. + docs + Whether to clean the *docs* directory. + bytecode + Whether to clean the bytecode files, e.g. *.pyc* files. + pytest + Whether to clean the *Pytest* cache directory. + """ + + message_box("Cleaning project...") + + patterns = ["build", "*.egg-info", "dist"] + + if docs: + patterns.append("docs/_build") + patterns.append("docs/generated") + + if bytecode: + patterns.append("**/__pycache__") + patterns.append("**/*.pyc") + + if pytest: + patterns.append(".pytest_cache") + + for pattern in patterns: + ctx.run(f"rm -rf {pattern}") + + +@task +def formatting( + ctx: Context, + asciify: bool = True, +): + """ + Convert unicode characters to ASCII and cleanup the *BibTeX* file. + + Parameters + ---------- + ctx + Context. + asciify + Whether to convert unicode characters to ASCII. + """ + + if asciify: + message_box("Converting unicode characters to ASCII...") + with ctx.cd("utilities"): + ctx.run("./unicode_to_ascii.py") + + +@task +def quality( + ctx: Context, + pyright: bool = True, + rstlint: bool = True, +): + """ + Check the codebase with *Pyright* and lints various *restructuredText* + files with *rst-lint*. + + Parameters + ---------- + ctx + Context. + pyright + Whether to check the codebase with *Pyright*. + rstlint + Whether to lint various *restructuredText* files with *rst-lint*. + """ + + if pyright: + message_box('Checking codebase with "Pyright"...') + ctx.run("pyright --skipunannotated --level warning") + + if rstlint: + message_box('Linting "README.rst" file...') + ctx.run("rst-lint README.rst") + + +@task +def precommit(ctx: Context): + """ + Run the "pre-commit" hooks on the codebase. + + Parameters + ---------- + ctx + Context. + """ + + message_box('Running "pre-commit" hooks on the codebase...') + ctx.run("pre-commit run --all-files") + + +@task +def tests(ctx: Context): + """ + Run the unit tests with *Pytest*. + + Parameters + ---------- + ctx + Context. + """ + + message_box('Running "Pytest"...') + ctx.run( + "pytest " + "--doctest-modules " + f"--ignore={PYTHON_PACKAGE_NAME}/examples " + f"--cov={PYTHON_PACKAGE_NAME} " + f"{PYTHON_PACKAGE_NAME}" + ) + + +@task +def examples(ctx: Context): + """ + Run the examples. + + Parameters + ---------- + ctx + Context. + """ + + message_box("Running examples...") + + for root, _dirnames, filenames in os.walk( + os.path.join(PYTHON_PACKAGE_NAME, "examples") + ): + for filename in fnmatch.filter(filenames, "*.py"): + ctx.run(f"python {os.path.join(root, filename)}") + + +@task(formatting, quality, precommit, tests, examples) +def preflight(ctx: Context): # noqa: ARG001 + """ + Perform the preflight tasks, i.e. *formatting*, *tests*, *quality*, and + *examples*. + + Parameters + ---------- + ctx + Context. + """ + + message_box('Finishing "Preflight"...') + + +@task +def docs( + ctx: Context, + plots: bool = True, + html: bool = True, + pdf: bool = True, +): + """ + Build the documentation. + + Parameters + ---------- + ctx + Context. + plots + Whether to generate the documentation plots. + html + Whether to build the *HTML* documentation. + pdf + Whether to build the *PDF* documentation. + """ + + if plots: + with ctx.cd("utilities"): + message_box("Generating plots...") + ctx.run("./generate_plots.py") + + with ctx.prefix("export COLOUR_SCIENCE__DOCUMENTATION_BUILD=True"), ctx.cd( + "docs" + ): + if html: + message_box('Building "HTML" documentation...') + ctx.run("make html") + + if pdf: + message_box('Building "PDF" documentation...') + ctx.run("make latexpdf") + + +@task +def todo(ctx: Context): + """ + Export the TODO items. + + Parameters + ---------- + ctx + Context. + """ + + message_box('Exporting "TODO" items...') + + with ctx.cd("utilities"): + ctx.run("./export_todo.py") + + +@task +def requirements(ctx: Context): + """ + Export the *requirements.txt* file. + + Parameters + ---------- + ctx + Context. + """ + + message_box('Exporting "requirements.txt" file...') + ctx.run( + "poetry export -f requirements.txt " + "--without-hashes " + "--with dev,docs,optional " + "--output requirements.txt" + ) + + message_box('Exporting "docs/requirements.txt" file...') + ctx.run( + "poetry export -f requirements.txt " + "--without-hashes " + "--with docs,optional " + "--output docs/requirements.txt" + ) + + +@task(clean, preflight, docs, todo, requirements) +def build(ctx: Context): + """ + Build the project and runs dependency tasks, i.e. *docs*, *todo*, and + *preflight*. + + Parameters + ---------- + ctx + Context. + """ + + message_box("Building...") + ctx.run("poetry build") + ctx.run("twine check dist/*") + + +@task +def virtualise(ctx: Context, tests: bool = True): + """ + Create a virtual environment for the project build. + + Parameters + ---------- + ctx + Context. + tests + Whether to run tests on the virtual environment. + """ + + unique_name = f"{PYPI_PACKAGE_NAME}-{uuid.uuid1()}" + with ctx.cd("dist"): + ctx.run(f"tar -xvf {PYPI_ARCHIVE_NAME}-{APPLICATION_VERSION}.tar.gz") + ctx.run(f"mv {PYPI_ARCHIVE_NAME}-{APPLICATION_VERSION} {unique_name}") + ctx.run(f"rm -rf {unique_name}/{PYTHON_PACKAGE_NAME}/resources") + ctx.run( + "ln -s ../../../{0}/resources {1}/{0}".format( + PYTHON_PACKAGE_NAME, unique_name + ) + ) + + with ctx.cd(unique_name): + ctx.run("poetry install") + ctx.run("source $(poetry env info -p)/bin/activate") + ctx.run( + 'python -c "import imageio;' + 'imageio.plugins.freeimage.download()"' + ) + if tests: + ctx.run( + "poetry run pytest " + "--doctest-modules " + f"--ignore={PYTHON_PACKAGE_NAME}/examples " + f"{PYTHON_PACKAGE_NAME}", + ) + + +@task +def tag(ctx: Context): + """ + Tag the repository according to defined version using *git-flow*. + + Parameters + ---------- + ctx + Context. + """ + + message_box("Tagging...") + result = ctx.run("git rev-parse --abbrev-ref HEAD", hide="both") + + if result.stdout.strip() != "develop": # pyright: ignore + raise RuntimeError("Are you still on a feature or master branch?") + + with open(os.path.join(PYTHON_PACKAGE_NAME, "__init__.py")) as file_handle: + file_content = file_handle.read() + major_version = re.search( + '__major_version__\\s+=\\s+"(.*)"', file_content + ).group( # pyright: ignore + 1 + ) + minor_version = re.search( + '__minor_version__\\s+=\\s+"(.*)"', file_content + ).group( # pyright: ignore + 1 + ) + change_version = re.search( + '__change_version__\\s+=\\s+"(.*)"', file_content + ).group( # pyright: ignore + 1 + ) + + version = ".".join((major_version, minor_version, change_version)) + + result = ctx.run("git ls-remote --tags upstream", hide="both") + remote_tags = result.stdout.strip().split("\n") # pyright: ignore + tags = set() + for remote_tag in remote_tags: + tags.add( + remote_tag.split("refs/tags/")[1].replace("refs/tags/", "^{}") + ) + version_tags = sorted(tags) + if f"v{version}" in version_tags: + raise RuntimeError( + f'A "{PYTHON_PACKAGE_NAME}" "v{version}" tag already exists in ' + f"remote repository!" + ) + + ctx.run(f"git flow release start v{version}") + ctx.run(f"git flow release finish v{version}") + + +@task(build) +def release(ctx: Context): + """ + Release the project to *Pypi* with *Twine*. + + Parameters + ---------- + ctx + Context. + """ + + message_box("Releasing...") + with ctx.cd("dist"): + ctx.run("twine upload *.tar.gz") + ctx.run("twine upload *.whl") + + +@task +def sha256(ctx: Context): + """ + Compute the project *Pypi* package *sha256* with *OpenSSL*. + + Parameters + ---------- + ctx + Context. + """ + + message_box('Computing "sha256"...') + with ctx.cd("dist"): + ctx.run(f"openssl sha256 {PYPI_ARCHIVE_NAME}-*.tar.gz") diff --git a/utilities/export_todo.py b/utilities/export_todo.py new file mode 100755 index 0000000..8c07d49 --- /dev/null +++ b/utilities/export_todo.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +""" +Export TODOs +============ +""" + +from __future__ import annotations + +import codecs +import os + +__copyright__ = "Copyright 2015 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "TODO_FILE_TEMPLATE", + "extract_todo_items", + "export_todo_items", +] + +TODO_FILE_TEMPLATE = """ +Colour - Visuals - TODO +======================= + +TODO +---- + +{0} + +About +----- + +| **Colour - Visuals** by Colour Developers +| Copyright 2023 Colour Developers - \ +`colour-developers@colour-science.org `__ +| This software is released under terms of BSD-3-Clause: \ +https://opensource.org/licenses/BSD-3-Clause +| `https://github.com/colour-science/colour-visuals \ +`__ +"""[ + 1: +] + + +def extract_todo_items(root_directory: str) -> dict: + """ + Extract the TODO items from given directory. + + Parameters + ---------- + root_directory + Directory to extract the TODO items from. + + Returns + ------- + :class:`dict` + TODO items. + """ + + todo_items = {} + for root, _dirnames, filenames in os.walk(root_directory): + for filename in filenames: + if not filename.endswith(".py"): + continue + + filename = os.path.join(root, filename) # noqa: PLW2901 + with codecs.open(filename, encoding="utf8") as file_handle: + content = file_handle.readlines() + + in_todo = False + line_number = 1 + todo_item = [] + for i, line in enumerate(content): + line = line.strip() # noqa: PLW2901 + if line.startswith("# TODO:"): + in_todo = True + line_number = i + 1 + todo_item.append(line) + continue + + if in_todo and line.startswith("#"): + todo_item.append(line.replace("#", "").strip()) + elif len(todo_item): + key = filename.replace("../", "") + if not todo_items.get(key): + todo_items[key] = [] + + todo_items[key].append((line_number, " ".join(todo_item))) + in_todo = False + todo_item = [] + + return todo_items + + +def export_todo_items(todo_items: dict, file_path: str): + """ + Export TODO items to given file. + + Parameters + ---------- + todo_items + TODO items. + file_path + File to write the TODO items to. + """ + + todo_rst = [] + for module, module_todo_items in todo_items.items(): + todo_rst.append(f"- {module}\n") + for line_numer, todo_item in module_todo_items: + todo_rst.append(f" - Line {line_numer} : {todo_item}") + + todo_rst.append("\n") + + with codecs.open(file_path, "w", encoding="utf8") as todo_file: + todo_file.write(TODO_FILE_TEMPLATE.format("\n".join(todo_rst[:-1]))) + + +if __name__ == "__main__": + os.chdir(os.path.dirname(__file__)) + + export_todo_items( + extract_todo_items(os.path.join("..", "colour_visuals")), + os.path.join("..", "TODO.rst"), + ) diff --git a/utilities/generate_plots.py b/utilities/generate_plots.py new file mode 100755 index 0000000..176719f --- /dev/null +++ b/utilities/generate_plots.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +""" +Generate Plots +============== +""" + +from __future__ import annotations + +import os + +import numpy as np +import pygfx as gfx +import pylinalg as la +from colour.io import write_image +from wgpu.gui.offscreen import WgpuCanvas + +from colour_visuals.diagrams import ( + VisualChromaticityDiagram, + VisualChromaticityDiagramCIE1931, + VisualChromaticityDiagramCIE1960UCS, + VisualChromaticityDiagramCIE1976UCS, + VisualSpectralLocus2D, + VisualSpectralLocus3D, +) +from colour_visuals.grid import VisualGrid +from colour_visuals.pointer_gamut import ( + VisualPointerGamut2D, + VisualPointerGamut3D, +) +from colour_visuals.rgb_colourspace import ( + VisualRGBColourspace2D, + VisualRGBColourspace3D, +) +from colour_visuals.rgb_scatter import VisualRGBScatter3D +from colour_visuals.rosch_macadam import VisualRoschMacAdam + +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "generate_documentation_plots", +] + + +def generate_documentation_plots(output_directory: str): + """ + Generate documentation plots. + + Parameters + ---------- + output_directory + Output directory. + """ + + # ************************************************************************* + # "README.rst" + # ************************************************************************* + canvas = WgpuCanvas(size=(960, 540)) + renderer = gfx.renderers.WgpuRenderer(canvas) + camera = gfx.PerspectiveCamera(50, 16 / 9) # pyright: ignore + + scene = gfx.Scene() + scene.add( + gfx.Background( + None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ) + ) + + for visual_class, arguments in [ + (VisualSpectralLocus2D, {}), + (VisualSpectralLocus3D, {}), + (VisualChromaticityDiagram, {}), + ( + VisualChromaticityDiagramCIE1931, + {"kwargs_visual_chromaticity_diagram": {"opacity": 0.25}}, + ), + ( + VisualChromaticityDiagramCIE1960UCS, + {"kwargs_visual_chromaticity_diagram": {"opacity": 0.25}}, + ), + ( + VisualChromaticityDiagramCIE1976UCS, + {"kwargs_visual_chromaticity_diagram": {"opacity": 0.25}}, + ), + (VisualGrid, {}), + (VisualPointerGamut2D, {}), + (VisualPointerGamut3D, {}), + (VisualRGBColourspace2D, {}), + (VisualRGBColourspace3D, {"wireframe": True}), + (VisualRGBScatter3D, {"RGB": np.random.random([24, 32, 3])}), + (VisualRoschMacAdam, {}), + ]: + visual = visual_class(**arguments) + + if isinstance( + visual, + (VisualRGBColourspace3D, VisualRGBScatter3D, VisualRoschMacAdam), + ): + visual.local.rotation = la.quat_from_euler( + (-np.pi / 4, 0), order="XY" + ) + + scene.add(visual) + camera.show_object( + visual, up=np.array([0, 0, 1]), scale=1.25 # pyright: ignore + ) + + canvas.request_draw(lambda: renderer.render(scene, camera)) + + write_image( + np.array(renderer.target.draw()), + os.path.join( + output_directory, f"Plotting_{visual.__class__.__name__}.png" + ), + bit_depth="uint8", + ) + scene.remove(visual) + + +if __name__ == "__main__": + os.chdir(os.path.dirname(__file__)) + + generate_documentation_plots(os.path.join("..", "docs", "_static")) diff --git a/utilities/unicode_to_ascii.py b/utilities/unicode_to_ascii.py new file mode 100755 index 0000000..4eb3ebb --- /dev/null +++ b/utilities/unicode_to_ascii.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +""" +Unicode to ASCII Utility +======================== +""" + +from __future__ import annotations + +import codecs +import os +import unicodedata + +__copyright__ = "Copyright 2015 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "SUBSTITUTIONS", + "unicode_to_ascii", +] + +SUBSTITUTIONS: dict[str, str] = { + "–": "-", + "“": '"', + "”": '"', + "‘": "'", + "’": "'", + "′": "'", +} + + +def unicode_to_ascii(root_directory: str): + """ + Recursively convert from unicode to ASCII *.py*, *.bib* and *.rst* files + in given directory. + + Parameters + ---------- + root_directory + Directory to convert the files from unicode to ASCII. + """ + + for root, _dirnames, filenames in os.walk(root_directory): + for filename in filenames: + if ( + not filename.endswith(".tex") + and not filename.endswith(".py") + and not filename.endswith(".bib") + and not filename.endswith(".rst") + ): + continue + + if filename == "unicode_to_ascii.py": + continue + + filename = os.path.join(root, filename) # noqa: PLW2901 + with codecs.open(filename, encoding="utf8") as file_handle: + content = file_handle.read() + + with codecs.open(filename, "w", encoding="utf8") as file_handle: + for key, value in SUBSTITUTIONS.items(): + content = content.replace(key, value) + + content = unicodedata.normalize("NFD", content) + + file_handle.write(content) + + +if __name__ == "__main__": + os.chdir(os.path.dirname(__file__)) + + unicode_to_ascii(os.path.join("..", "colour_visuals"))