From 1f4cb8e25f96806111f8b6e5a7f5552625e738aa Mon Sep 17 00:00:00 2001 From: Thomas Mansencal Date: Sun, 15 Oct 2023 17:31:39 +1300 Subject: [PATCH] Initial commit. --- .github/ISSUE_TEMPLATE/bug-report.yml | 43 ++ .../documentation-improvement.yml | 40 ++ .github/ISSUE_TEMPLATE/feature-request.yml | 18 + .github/ISSUE_TEMPLATE/question.yml | 17 + .github/PULL_REQUEST_TEMPLATE.md | 31 ++ .github/funding.yml | 12 + .../continuous-integration-documentation.yml | 48 ++ ...tinuous-integration-quality-unit-tests.yml | 62 +++ ...nuous-integration-static-type-checking.yml | 29 ++ .gitignore | 20 + .gitmodules | 6 + .pre-commit-config.yaml | 19 + .readthedocs.yaml | 17 + CODE_OF_CONDUCT.md | 51 ++ CONTRIBUTORS.rst | 20 + LICENSE | 11 + README.rst | 89 ++++ TODO.rst | 13 + colour_visuals/__init__.py | 105 ++++ colour_visuals/common.py | 101 ++++ colour_visuals/diagrams.py | 474 ++++++++++++++++++ colour_visuals/grid.py | 212 ++++++++ colour_visuals/pointer_gamut.py | 213 ++++++++ colour_visuals/py.typed | 0 colour_visuals/rgb_colourspace.py | 203 ++++++++ colour_visuals/rgb_scatter.py | 109 ++++ colour_visuals/rosch_macadam.py | 148 ++++++ docs/Makefile | 177 +++++++ docs/_static/Logo_Light_001.svg | 71 +++ docs/_static/Logo_Medium_001.png | Bin 0 -> 53455 bytes .../Plotting_VisualSpectralLocus2D.png | Bin 0 -> 33574 bytes docs/_templates/class.rst | 8 + docs/colour_visuals.rst | 15 + docs/conf.py | 188 +++++++ docs/index.rst | 68 +++ docs/installation.rst | 8 + docs/make.bat | 242 +++++++++ docs/reference.rst | 13 + docs/requirements.txt | 50 ++ docs/user-guide.rst | 13 + pyproject.toml | 193 +++++++ requirements.txt | 180 +++++++ tasks.py | 443 ++++++++++++++++ utilities/export_todo.py | 128 +++++ utilities/generate_plots.py | 70 +++ utilities/unicode_to_ascii.py | 74 +++ 46 files changed, 4052 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation-improvement.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/funding.yml create mode 100644 .github/workflows/continuous-integration-documentation.yml create mode 100644 .github/workflows/continuous-integration-quality-unit-tests.yml create mode 100644 .github/workflows/continuous-integration-static-type-checking.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTORS.rst create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 TODO.rst create mode 100644 colour_visuals/__init__.py create mode 100644 colour_visuals/common.py create mode 100644 colour_visuals/diagrams.py create mode 100644 colour_visuals/grid.py create mode 100644 colour_visuals/pointer_gamut.py create mode 100644 colour_visuals/py.typed create mode 100644 colour_visuals/rgb_colourspace.py create mode 100644 colour_visuals/rgb_scatter.py create mode 100644 colour_visuals/rosch_macadam.py create mode 100644 docs/Makefile create mode 100644 docs/_static/Logo_Light_001.svg create mode 100644 docs/_static/Logo_Medium_001.png create mode 100644 docs/_static/Plotting_VisualSpectralLocus2D.png create mode 100644 docs/_templates/class.rst create mode 100644 docs/colour_visuals.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/installation.rst create mode 100644 docs/make.bat create mode 100644 docs/reference.rst create mode 100644 docs/requirements.txt create mode 100644 docs/user-guide.rst create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 tasks.py create mode 100755 utilities/export_todo.py create mode 100755 utilities/generate_plots.py create mode 100755 utilities/unicode_to_ascii.py 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..f6b3d98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +*.egg-info +*.pyc +*.pyo +.DS_Store +.coverage* +.fleet +.idea +.ipynb_checkpoints +.sandbox +.vs +.vscode + +__pycache__ + +build +colour_visuals.egg-info +dist +docs/_build +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..b527df6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: +- repo: https://github.com/ikamensh/flynt/ + rev: '1.0.1' + hooks: + - id: flynt +- 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..a9c435b --- /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 numpy as np +import os +import subprocess + +import colour + +from .diagrams import ( + VisualSpectralLocus2D, + VisualSpectralLocus3D, + VisualChromaticityDiagram, + VisualChromaticityDiagramCIE1931, + VisualChromaticityDiagramCIE1960UCS, + VisualChromaticityDiagramCIE1976UCS, +) +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..41409ec --- /dev/null +++ b/colour_visuals/common.py @@ -0,0 +1,101 @@ +""" +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, 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_alpha_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: str, **kwargs +) -> NDArray: + """ + Converts from *CIE XYZ* tristimulus values to given colourspace model while + normalising some of the absolute 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): + 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 dtype. + """ + + 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_alpha_channel(a: ArrayLike, alpha: float = 1) -> NDArray: + a = np.copy(a) + + return np.hstack([a, full(list(a.shape[:-1]) + [1], alpha, dtype=a.dtype)]) diff --git a/colour_visuals/diagrams.py b/colour_visuals/diagrams.py new file mode 100644 index 0000000..c7670f4 --- /dev/null +++ b/colour_visuals/diagrams.py @@ -0,0 +1,474 @@ +# !/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 scipy.spatial import Delaunay + +from colour.algebra import euclidean_distance, normalise_maximum +from colour.colorimetry import MultiSpectralDistributions +from colour.hints import ArrayLike, Optional, 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 colour_visuals.common import ( + DEFAULT_FLOAT_DTYPE_WGPU, + DEFAULT_INT_DTYPE_WGPU, + append_alpha_channel, + as_contiguous_array, + XYZ_to_colourspace_model, +) + +__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 *Spectral Locus* 2D visual. + + Examples + -------- + >>> from wgpu.gui.auto import WgpuCanvas + >>> canvas = WgpuCanvas(size=(960, 540)) + >>> scene = gfx.Scene() + >>> scene.add( + ... gfx.Background( + ... None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ... ) + ... ) # doctest: +ELLIPSIS + + >>> visual = VisualSpectralLocus2D() + >>> camera = gfx.PerspectiveCamera(50, 16 / 9) + >>> camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + >>> scene.add(visual) # doctest: +ELLIPSIS + + >>> gfx.show(scene, camera=camera, canvas=canvas) + + .. image:: ../_static/Plotting_VisualSpectralLocus2D.png + :align: center + :alt: visualspectrallocus2d + """ + + def __init__( + self, + cmfs: str = "CIE 1931 2 Degree Standard Observer", + method: str = "CIE 1931", + labels: Sequence | None = None, + colors: 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 = 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 colors is None: + colors_sl = np.concatenate( + [lines_sl["colour"][:-1], lines_sl["colour"][1:]], axis=1 + ).reshape([-1, 3]) + else: + colors_sl = np.tile(colors, (positions.shape[0], 1)) + + self._spectral_locus = gfx.Line( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array( + append_alpha_channel(colors_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 colors is None: + colors_w = lines_w["colour"] + else: + colors_w = np.tile(colors, (positions.shape[0], 1)) + + self._wavelengths = gfx.Line( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array( + append_alpha_channel(colors_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 colors is None: + colors_lp = lines_w["colour"][::2] + else: + colors_lp = np.tile(colors, (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_alpha_channel(colors_lp, opacity) + ), + ), + gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), + ) + self.add(self._points) + + +class VisualSpectralLocus3D(gfx.Line): + """ + Create a *Spectral Locus* 3D visual. + """ + + def __init__( + self, + cmfs: str = "CIE 1931 2 Degree Standard Observer", + colourspace_model: str = "CIE xyY", + colors: 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, colourspace_model + ), + colourspace_model, + ) + positions = np.concatenate( + [positions[:-1], positions[1:]], axis=1 + ).reshape([-1, 3]) + + if colors is None: + colors = XYZ_to_RGB(cmfs.values, colourspace) + colors = np.concatenate([colors[:-1], colors[1:]], axis=1).reshape( + [-1, 3] + ) + else: + colors = np.tile(colors, (positions.shape[0], 1)) + + super().__init__( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array( + append_alpha_channel(colors, opacity) + ), + ), + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + + +class VisualChromaticityDiagram(gfx.Mesh): + """ + Create a *Chromaticity Diagram* visual. + """ + + def __init__( + self, + samples=64, + cmfs: str = "CIE 1931 2 Degree Standard Observer", + method: str = "CIE 1931", + colors: Optional[ArrayLike] = None, + opacity: float = 1, + material: Type[gfx.MeshAbstractMaterial] = gfx.MeshBasicMaterial, + wireframe: bool = False, + ): + cmfs = 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") + samples = np.linspace(0, 1, samples) + ii_g, jj_g = np.meshgrid(samples, samples) + 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 colors is None: + colors = normalise_maximum( + XYZ_to_plotting_colourspace( + ij_to_XYZ(positions[..., :2], illuminant), illuminant + ), + axis=-1, + ) + else: + colors = np.tile(colors, (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_alpha_channel(colors, 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. + """ + + def __init__( + self, + kwargs_visual_chromaticity_diagram: Optional[dict] = None, + kwargs_visual_spectral_locus: Optional[dict] = None, + ): + super().__init__() + + self._chromaticity_diagram = VisualChromaticityDiagram( + method="CIE 1931", + **(optional(kwargs_visual_chromaticity_diagram, {})), + ) + self.add(self._chromaticity_diagram) + + self._spectral_locus = VisualSpectralLocus2D( + method="CIE 1931", **(optional(kwargs_visual_spectral_locus, {})) + ) + self.add(self._spectral_locus) + + +class VisualChromaticityDiagramCIE1960UCS(gfx.Group): + """ + Create the *CIE 1960 UCS* chromaticity diagram visual. + """ + + def __init__( + self, + kwargs_visual_chromaticity_diagram: Optional[dict] = None, + kwargs_visual_spectral_locus: Optional[dict] = None, + ): + super().__init__() + + self._chromaticity_diagram = VisualChromaticityDiagram( + method="CIE 1960 UCS", + **(optional(kwargs_visual_chromaticity_diagram, {})), + ) + self.add(self._chromaticity_diagram) + + self._spectral_locus = VisualSpectralLocus2D( + method="CIE 1960 UCS", + **(optional(kwargs_visual_spectral_locus, {})), + ) + self.add(self._spectral_locus) + + +class VisualChromaticityDiagramCIE1976UCS(gfx.Group): + """ + Create the *CIE 1976 UCS* chromaticity diagram visual. + """ + + def __init__( + self, + kwargs_visual_chromaticity_diagram: Optional[dict] = None, + kwargs_visual_spectral_locus: Optional[dict] = None, + ): + super().__init__() + + self._chromaticity_diagram = VisualChromaticityDiagram( + method="CIE 1976 UCS", + **(optional(kwargs_visual_chromaticity_diagram, {})), + ) + self.add(self._chromaticity_diagram) + + self._spectral_locus = VisualSpectralLocus2D( + method="CIE 1976 UCS", + **(optional(kwargs_visual_spectral_locus, {})), + ) + self.add(self._spectral_locus) + + +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={"colors": [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(colors=[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(colors=[0.5, 0.5, 0.5]) + visual_8.local.position = np.array([5, 0, 0]) + scene.add(visual_8) + + visual_9 = VisualSpectralLocus3D(colourspace_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..3c5d59b --- /dev/null +++ b/colour_visuals/grid.py @@ -0,0 +1,212 @@ +# !/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.hints import ArrayLike +from colour.geometry import primitive_grid +from colour.plotting import CONSTANTS_COLOUR_STYLE + +from colour_visuals.common import ( + DEFAULT_FLOAT_DTYPE_WGPU, + append_alpha_channel, + conform_primitive_dtype, + 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__ = ["VisualGrid"] + + +class VisualGrid(gfx.Group): + """ + Create a *RGB* 3D scatter visual. + """ + + def __init__( + self, + size: int = 20, + major_grid_color: ArrayLike = np.array([0.5, 0.5, 0.5]), + minor_grid_color: ArrayLike = np.array([0.25, 0.25, 0.25]), + major_tick_labels=True, + major_tick_label_color: ArrayLike = np.array([0.75, 0.75, 0.75]), + minor_tick_labels=True, + minor_tick_label_color: 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_alpha_channel( + np.tile(major_grid_color, (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_alpha_channel( + np.tile(minor_grid_color, (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_colors = 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_colors), + 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_color), + ) + 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_color), + ) + 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) + + 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_color), + ) + 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_color), + ) + 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..a5a79fa --- /dev/null +++ b/colour_visuals/pointer_gamut.py @@ -0,0 +1,213 @@ +# !/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 +from colour.models import ( + CCS_ILLUMINANT_POINTER_GAMUT, + DATA_POINTER_GAMUT_VOLUME, + LCHab_to_Lab, + Lab_to_XYZ, +) +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, + append_alpha_channel, + as_contiguous_array, + XYZ_to_colourspace_model, +) + +__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 *Pointer's Gamut* 2D visual. + """ + + def __init__( + self, + method: str = "CIE 1931", + colors: ArrayLike | str | 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 colors is None: + colors_b = np.concatenate( + [lines_b["colour"][:-1], lines_b["colour"][1:]], axis=1 + ).reshape([-1, 3]) + else: + colors_b = np.tile(colors, (positions.shape[0], 1)) + + self._pointer_gamut_boundary = gfx.Line( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array( + append_alpha_channel(colors_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 colors is None: + colors_v = lines_v["colour"] + else: + colors_v = np.tile(colors, (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_alpha_channel(colors_v, opacity) + ), + ), + gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), + ) + self.add(self._pointer_gamut_volume) + + +class VisualPointerGamut3D(gfx.Line): + """ + Create a *Pointer's Gamut* 3D visual. + """ + + def __init__( + self, + colourspace_model: str = "CIE xyY", + segments: int = 16, + colors: 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( + 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, + colourspace_model, + ), + colourspace_model, + ).reshape([-1, 3]) + + if colors is None: + colors = XYZ_to_plotting_colourspace(sections, illuminant).reshape( + [-1, 3] + ) + else: + colors = np.tile(colors, (positions.shape[0], 1)) + + super().__init__( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array( + append_alpha_channel(colors, 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(colors=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..bacbeac --- /dev/null +++ b/colour_visuals/rgb_colourspace.py @@ -0,0 +1,203 @@ +# !/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, Type +from colour.models import 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 + +from colour_visuals.common import ( + append_alpha_channel, + as_contiguous_array, + conform_primitive_dtype, + XYZ_to_colourspace_model, +) + +__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.Line): + """ + Create a *RGB* colourspace 2D gamut visual. + """ + + def __init__( + self, + colourspace: str = "ITU-R BT.709", + chromaticity_diagram: str = "CIE 1931", + colors: ArrayLike | None = None, + opacity: float = 0.5, + thickness: float = 1, + ): + colourspace = first_item(filter_RGB_colourspaces(colourspace).values()) + + plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace + + XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[chromaticity_diagram][ + "XYZ_to_ij" + ] + + ij = XYZ_to_ij( + xy_to_XYZ(colourspace.primaries), plotting_colourspace.whitepoint + ) + ij[np.isnan(ij)] = 0 + + positions = append_alpha_channel( + np.array([ij[0], ij[1], ij[1], ij[2], ij[2], ij[0]]), 0 + ) + + if colors is None: + RGB = XYZ_to_RGB( + xy_to_XYZ(colourspace.primaries), plotting_colourspace + ) + colors = np.array([RGB[0], RGB[1], RGB[1], RGB[2], RGB[2], RGB[0]]) + else: + colors = np.tile(colors, (positions.shape[0], 1)) + + geometry = gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array(append_alpha_channel(colors, opacity)), + ) + + super().__init__( + geometry, + gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + ) + + +class VisualRGBColourspace3D(gfx.Mesh): + """ + Create a *RGB* colourspace 3D volume visual. + """ + + def __init__( + self, + colourspace: str = "ITU-R BT.709", + colourspace_model: str = "CIE xyY", + segments: int = 16, + colors: ArrayLike | None = None, + opacity: float = 0.5, + material: Type[gfx.MeshAbstractMaterial] = gfx.MeshBasicMaterial, + wireframe: bool = False, + ): + 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 colors is None: + colors = positions + else: + colors = np.tile(colors, (positions.shape[0], 1)) + + positions = colourspace_model_axis_reorder( + XYZ_to_colourspace_model( + RGB_to_XYZ(positions, colourspace), + colourspace.whitepoint, + colourspace_model, + ), + colourspace_model, + ) + + geometry = gfx.Geometry( + positions=as_contiguous_array(positions), + normals=vertices["normal"], + indices=outline[..., 1].reshape([-1, 4]), + colors=as_contiguous_array(append_alpha_channel(colors, opacity)), + ) + + super().__init__( + geometry, + 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( + colourspace_model="CIE Lab", + colors=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( + chromaticity_diagram="CIE 1976 UCS", + colors=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..6549d96 --- /dev/null +++ b/colour_visuals/rgb_scatter.py @@ -0,0 +1,109 @@ +# !/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, Type +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 ( + append_alpha_channel, + as_contiguous_array, + XYZ_to_colourspace_model, +) + +__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 *RGB* 3D scatter visual. + """ + + def __init__( + self, + RGB: ArrayLike, + colourspace: str = "ITU-R BT.709", + colourspace_model: str = "CIE xyY", + colors: ArrayLike | None = None, + opacity: float = 0.5, + size: float = 2, + ): + 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, colourspace_model + ), + colourspace_model, + ) + + if colors is None: + colors = RGB + else: + colors = np.tile(colors, (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_alpha_channel(colors, 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)), colors=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..5aa1f43 --- /dev/null +++ b/colour_visuals/rosch_macadam.py @@ -0,0 +1,148 @@ +# !/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.constants import EPSILON +from colour.colorimetry import ( + MultiSpectralDistributions, + SpectralDistribution, + SpectralShape, +) +from colour.hints import ArrayLike, 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 ( + append_alpha_channel, + as_contiguous_array, + XYZ_to_colourspace_model, +) + +__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 *Visible Spectrum* 3D visual. + """ + + def __init__( + self, + cmfs: str = "CIE 1931 2 Degree Standard Observer", + illuminant: str = "E", + colourspace_model: str = "CIE xyY", + colors: 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, colourspace_model + ), + colourspace_model, + ) + positions = np.concatenate( + [positions[:-1], positions[1:]], axis=1 + ).reshape([-1, 3]) + + if colors is None: + colors = XYZ_to_RGB(XYZ, colourspace) + colors = np.concatenate([colors[:-1], colors[1:]], axis=1).reshape( + [-1, 3] + ) + else: + colors = np.tile(colors, (positions.shape[0], 1)) + + super().__init__( + gfx.Geometry( + positions=as_contiguous_array(positions), + colors=as_contiguous_array( + append_alpha_channel(colors, 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( + colourspace_model="CIE XYZ", colors=np.array([0.5, 0.5, 0.5]) + ) + visual_2.local.position = np.array([1, 0, 0]) + scene.add(visual_2) + + visual_3 = VisualRoschMacAdam( + colourspace_model="JzAzBz", colors=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( + colourspace_model="ICtCp", colors=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_Light_001.svg b/docs/_static/Logo_Light_001.svg new file mode 100644 index 0000000..3d7da27 --- /dev/null +++ b/docs/_static/Logo_Light_001.svg @@ -0,0 +1,71 @@ + + + + + + + + + diff --git a/docs/_static/Logo_Medium_001.png b/docs/_static/Logo_Medium_001.png new file mode 100644 index 0000000000000000000000000000000000000000..ca330432edbdbaf5d18369dfc7fcebd43cdddbcc GIT binary patch literal 53455 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%0AR#w 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 H+sOY17e^!= literal 0 HcmV?d00001 diff --git a/docs/_static/Plotting_VisualSpectralLocus2D.png b/docs/_static/Plotting_VisualSpectralLocus2D.png new file mode 100644 index 0000000000000000000000000000000000000000..51669e99e156b94dc8149b2676dabfb5eabbc81e GIT binary patch literal 33574 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 Q0R#TXJyMn`cxdeZKe~^kKL7v# literal 0 HcmV?d00001 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..e900e6e --- /dev/null +++ b/docs/colour_visuals.rst @@ -0,0 +1,15 @@ +Colour - Visuals +================ + +Chromaticity Diagrams +--------------------- + +``colour_visuals`` + +.. currentmodule:: colour_visuals + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + VisualSpectralLocus2D \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..d8327b2 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,188 @@ +""" +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.11", None), + "matplotlib": ("https://matplotlib.org/stable", None), + "numpy": ("https://numpy.org/doc/stable", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/dev", None), + "scipy": ("https://docs.scipy.org/doc/scipy-1.8.0/", 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..52893bf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,193 @@ +[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.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..85b8f91 --- /dev/null +++ b/tasks.py @@ -0,0 +1,443 @@ +""" +Invoke - Tasks +============== +""" + +from __future__ import annotations + +import fnmatch +import os +import re +import uuid + +import colour_visuals +from colour.utilities import message_box + +import inspect + +if not hasattr(inspect, "getargspec"): + inspect.getargspec = inspect.getfullargspec # pyright: ignore + +from invoke.tasks import task +from invoke.context import Context + +__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, html: bool = True, pdf: bool = True): + """ + Build the documentation. + + Parameters + ---------- + ctx + Context. + html + Whether to build the *HTML* documentation. + pdf + Whether to build the *PDF* documentation. + """ + + 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..8197abc --- /dev/null +++ b/utilities/generate_plots.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +""" +Generate Plots +============== +""" + +from __future__ import annotations + +import numpy as np +import os +import pygfx as gfx +from colour.io import write_image +from colour.utilities import as_float_array +from wgpu.gui.offscreen import WgpuCanvas + +from colour_visuals import VisualSpectralLocus2D + +__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) + + scene = gfx.Scene() + + scene.add( + gfx.Background( + None, gfx.BackgroundMaterial(np.array([0.18, 0.18, 0.18])) + ) + ) + visual = VisualSpectralLocus2D() + scene.add(visual) + + camera = gfx.PerspectiveCamera(50, 16 / 9) + camera.show_object(visual, up=np.array([0, 0, 1]), scale=1.25) + + canvas.request_draw(lambda: renderer.render(scene, camera)) + + write_image( + np.array(renderer.target.draw()), + os.path.join(output_directory, "Plotting_VisualSpectralLocus2D.png"), + bit_depth="uint8" + ) + + +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"))