squash commits
This commit is contained in:
commit
0f545c5e4d
611 files changed
+272565
No files matched your search
@@ -0,0 +1,8 @@
|
||||
node_modules/*
|
||||
shared-assets
|
||||
dist/*
|
||||
lib/*
|
||||
**/*.sw*
|
||||
.DS_Store
|
||||
.idea
|
||||
*.iml
|
||||
@@ -0,0 +1,93 @@
|
||||
# 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, 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 both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. 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.
|
||||
|
||||
This Code of Conduct also applies outside the project spaces when the Project
|
||||
Steward has a reasonable belief that an individual's behavior may have a
|
||||
negative impact on the project or its community.
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
We do not believe that all conflict is bad; healthy debate and disagreement
|
||||
often yield positive results. However, it is never okay to be disrespectful or
|
||||
to engage in behavior that violates the project’s code of conduct.
|
||||
|
||||
If you see someone violating the code of conduct, you are encouraged to address
|
||||
the behavior directly with those involved. Many issues can be resolved quickly
|
||||
and easily, and this gives people more control over the outcome of their
|
||||
dispute. If you are unable to resolve the matter for any reason, or if the
|
||||
behavior is threatening or harassing, report it. We are dedicated to providing
|
||||
an environment where participants feel welcome and safe.
|
||||
|
||||
Reports should be directed to Matt Small (mbsmall@google.com), the
|
||||
Project Steward(s) for model-viewer. It is the Project Steward’s duty to
|
||||
receive and address reported violations of the code of conduct. They will then
|
||||
work with a committee consisting of representatives from the Open Source
|
||||
Programs Office and the Google Open Source Strategy team. If for any reason you
|
||||
are uncomfortable reaching out the Project Steward, please email
|
||||
opensource@google.com.
|
||||
|
||||
We will investigate every complaint, but you may not receive a direct response.
|
||||
We will use our discretion in determining when and how to follow up on reported
|
||||
incidents, which may range from not taking action to permanent expulsion from
|
||||
the project and project-sponsored spaces. We will notify the accused of the
|
||||
report and provide them an opportunity to discuss it before any action is taken.
|
||||
The identity of the reporter will be omitted from the details of the report
|
||||
supplied to the accused. In potentially harmful situations, such as ongoing
|
||||
harassment or threats to anyone's safety, we may take action without notice.
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,99 @@
|
||||
# `<model-viewer-effects>`
|
||||
|
||||

|
||||

|
||||
|
||||
`<model-viewer-effects>` is a web component library addon for `<model-viewer>` that makes adding post-processing
|
||||
effects to your models easy to do, on as many browsers and devices as possible.
|
||||
|
||||
`<model-viewer-effects>` strives to give you great defaults for rendering quality and
|
||||
performance.
|
||||
|
||||

|
||||
|
||||
[Examples](https://modelviewer.dev/examples/postprocessing/) • [Documentation](https://modelviewer.dev/docs/mve)
|
||||
## Usage
|
||||
Using effects is as simple as adding the `<effect-composer>` inside your `<model-viewer>`, and placing any effects inside the composer component.
|
||||
|
||||
```html
|
||||
<model-viewer src="...">
|
||||
<effect-composer>
|
||||
<bloom-effect></bloom-effect>
|
||||
</effect-composer>
|
||||
</model-viewer>
|
||||
```
|
||||
|
||||
### PostProcessing
|
||||
`<model-viewer-effects>` uses the [postprocessing](https://github.com/pmndrs/postprocessing) library under the hood, for its superior [performance](https://github.com/pmndrs/postprocessing#performance) and support.
|
||||
|
||||
In addition to the built-in effects wrapped by this library, you can add any custom effects/passes that follow the [postprocessing spec](https://github.com/pmndrs/postprocessing/wiki/Custom-Passes).
|
||||
|
||||
### *XR Support*
|
||||
The effects are not supported in the `<model-viewer>` XR modes, which will render as usual.
|
||||
|
||||
## Installing
|
||||
### NPM
|
||||
|
||||
The `<model-viewer-effects>` library can be installed from [NPM](https://npmjs.org):
|
||||
|
||||
```sh
|
||||
npm install three @google/model-viewer @google/model-viewer-effects
|
||||
```
|
||||
|
||||
### HTML
|
||||
|
||||
`<model-viewer-effects>` and `<model-viewer>` share a [Three.js](https://threejs.org/) dependency. In order to avoid version conflicts, you should bring Three through an `import-map`:
|
||||
|
||||
```html
|
||||
<!-- ES-Shims for older browser compatibility -->
|
||||
<script async src="https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.js"></script>
|
||||
|
||||
<!-- Import Three.js using an import-map -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@^0.172.0/build/three.module.min.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
You should then bring the `module` version of `<model-viewer>`, along with `<model-viewer-effects>` from your favourite CDN, such as [jsDelivr](https://www.jsdelivr.com/package/npm/@google/model-viewer):
|
||||
|
||||
|
||||
```html
|
||||
<script type="module" src=" https://cdn.jsdelivr.net/npm/@google/model-viewer/dist/model-viewer-module.min.js "></script>
|
||||
|
||||
<script type="module" src=" https://cdn.jsdelivr.net/npm/@google/model-viewer-effects/dist/model-viewer-effects.min.js "></script>
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
`<model-viewer-effects>` is supported on the last 2 major versions of all evergreen
|
||||
desktop and mobile browsers, and on all platforms (Android, IOS, MacOS, Windows, Linux).
|
||||
|
||||
| | <img src="https://github.com/alrra/browser-logos/raw/master/src/chrome/chrome_32x32.png" width="16"> Chrome | <img src="https://github.com/alrra/browser-logos/raw/master/src/firefox/firefox_32x32.png" width="16"> Firefox | <img src="https://github.com/alrra/browser-logos/raw/master/src/safari/safari_32x32.png" width="16"> Safari | <img src="https://github.com/alrra/browser-logos/raw/master/src/edge/edge_32x32.png" width="16"> Edge |
|
||||
| -------- | --- | --- | --- | --- |
|
||||
| Desktop | ✅ | ✅ | ✅ | ✅ |
|
||||
| Mobile | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
`<model-viewer-effects>` builds upon standard web platform APIs so that the performance,
|
||||
capabilities and compatibility of the library get better as the web evolves.
|
||||
|
||||
## Development
|
||||
|
||||
To get started, follow the instructions in [the main README.md file](../../README.md).
|
||||
|
||||
The following commands are available when developing `<model-viewer-effects>`:
|
||||
|
||||
Command | Description
|
||||
------------------------------- | -----------
|
||||
`npm run build` | Builds all `<model-viewer-effects>` distributable files
|
||||
`npm run build:dev` | Builds a subset of distributable files (faster than `npm run build`)
|
||||
`npm run test` | Run `<model-viewer-effects>` unit tests
|
||||
`npm run clean` | Deletes all build artifacts
|
||||
`npm run dev` | Starts `tsc` and `rollup` in "watch" mode, causing artifacts to automatically rebuild upon incremental changes
|
||||
|
||||
|
||||
----
|
||||
*Rocket Ship by Daniel Melchior [CC-BY](https://creativecommons.org/licenses/by/3.0/) via [Poly Pizza](https://poly.pizza/m/9dyJn4gp7U8)*
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"name": "@google/model-viewer-effects",
|
||||
"type": "module",
|
||||
"version": "1.4.0",
|
||||
"description": "Easily add and combine post-processing effects with <model-viewer>!",
|
||||
"repository": "https://github.com/google/model-viewer/tree/master/packages/model-viewer-effects",
|
||||
"bugs": {
|
||||
"url": "https://github.com/google/model-viewer/issues"
|
||||
},
|
||||
"homepage": "https://github.com/google/model-viewer/tree/master/packages/model-viewer-effects#readme",
|
||||
"contributors": [
|
||||
"Adam Beili <[email protected]>"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"main": "dist/model-viewer-effects.min.js",
|
||||
"module": "lib/model-viewer-effects.js",
|
||||
"files": [
|
||||
"src",
|
||||
"lib",
|
||||
"dist/model-viewer-effects.js",
|
||||
"dist/model-viewer-effects.js.map",
|
||||
"dist/model-viewer-effects.min.js",
|
||||
"dist/model-viewer-effects.min.js.map",
|
||||
"dist/model-viewer-effects-umd.js",
|
||||
"dist/model-viewer-effects-umd.js.map",
|
||||
"dist/model-viewer-effects-umd.min.js",
|
||||
"dist/model-viewer-effects-umd.min.js.map",
|
||||
"dist/model-viewer-effects.d.ts"
|
||||
],
|
||||
"typings": "lib/model-viewer-effects.d.ts",
|
||||
"types": "lib/model-viewer-effects.d.ts",
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./lib ./dist",
|
||||
"prepare": "if [ ! -L './shared-assets' ]; then ln -s ../shared-assets ./shared-assets; fi && ../shared-assets/scripts/fetch-khronos-gltf-samples.sh",
|
||||
"build": "npm run build:tsc && npm run build:rollup",
|
||||
"build:dev": "npm run build:tsc && npm run build:rollup:dev",
|
||||
"build:tsc": "tsc --incremental",
|
||||
"build:rollup": "rollup -c --environment NODE_ENV:production",
|
||||
"build:rollup:dev": "rollup -c --environment NODE_ENV:development",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "web-test-runner --playwright --browsers chromium firefox webkit",
|
||||
"test:ci": "web-test-runner --static-logging --playwright --browsers chromium webkit",
|
||||
"serve": "node_modules/.bin/http-server -c-1",
|
||||
"dev": "npm run build:dev && npm-run-all --parallel 'watch:tsc -- --preserveWatchOutput' 'watch:test' 'serve -- -s'",
|
||||
"watch:tsc": "tsc -w --incremental",
|
||||
"watch:rollup": "rollup -c -w --environment NODE_ENV:production",
|
||||
"watch:rollup:dev": "rollup -c -w --environment NODE_ENV:development",
|
||||
"watch:test": "web-test-runner --node-resolve --playwright --browsers chromium --watch",
|
||||
"build:dev:serve": "npm run build:dev && npm run serve"
|
||||
},
|
||||
"keywords": [
|
||||
"ar",
|
||||
"gltf",
|
||||
"glb",
|
||||
"webar",
|
||||
"webvr",
|
||||
"webxr",
|
||||
"arcore",
|
||||
"arkit",
|
||||
"webaronarcore",
|
||||
"webaronarkit",
|
||||
"augmented reality",
|
||||
"model-viewer",
|
||||
"model-viewer-effects",
|
||||
"3d",
|
||||
"post",
|
||||
"processing",
|
||||
"effect",
|
||||
"filter"
|
||||
],
|
||||
"dependencies": {
|
||||
"lit": "^3.2.1",
|
||||
"postprocessing": "^6.37.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@google/model-viewer": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@google/model-viewer": "^4.1.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@rollup/plugin-replace": "^6.0.2",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/three": "^0.174.0",
|
||||
"@ungap/event-target": "^0.2.4",
|
||||
"@web/test-runner": "^0.20.0",
|
||||
"@web/test-runner-playwright": "^0.11.0",
|
||||
"chai": "^5.2.0",
|
||||
"@rollup/plugin-swc": "^0.4.0",
|
||||
"@swc/core": "^1.11.8",
|
||||
"focus-visible": "^5.2.1",
|
||||
"http-server": "^14.1.1",
|
||||
"mocha": "^11.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup": "^4.35.0",
|
||||
"rollup-plugin-cleanup": "^3.2.1",
|
||||
"rollup-plugin-dts": "^6.1.1",
|
||||
"rollup-plugin-polyfill": "^4.2.0",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"three": "^0.174.0",
|
||||
"typescript": "5.8.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import {nodeResolve as resolve} from '@rollup/plugin-node-resolve';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import swc from '@rollup/plugin-swc';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import cleanup from 'rollup-plugin-cleanup';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
|
||||
const {NODE_ENV} = process.env;
|
||||
|
||||
const onwarn = (warning, warn) => {
|
||||
// Suppress non-actionable warning caused by TypeScript boilerplate:
|
||||
if (warning.code !== 'THIS_IS_UNDEFINED') {
|
||||
warn(warning);
|
||||
}
|
||||
};
|
||||
|
||||
let plugins = [
|
||||
resolve(),
|
||||
replace({'Reflect.decorate': 'undefined', preventAssignment: true})
|
||||
];
|
||||
|
||||
const watchFiles = ['lib/**'];
|
||||
|
||||
const outputOptions = [
|
||||
{
|
||||
input: './lib/model-viewer-effects.js',
|
||||
output: {
|
||||
file: './dist/model-viewer-effects.js',
|
||||
sourcemap: true,
|
||||
format: 'esm',
|
||||
name: 'ModelViewerEffects',
|
||||
globals: {
|
||||
three: 'three',
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
include: watchFiles,
|
||||
},
|
||||
plugins,
|
||||
external: ['three'],
|
||||
onwarn,
|
||||
},
|
||||
];
|
||||
|
||||
if (NODE_ENV !== 'development') {
|
||||
const pluginsIE11 = [
|
||||
...plugins,
|
||||
commonjs(),
|
||||
swc(),
|
||||
cleanup({
|
||||
// Ideally we'd also clean third_party/three, which saves
|
||||
// ~45kb in filesize alone... but takes 2 minutes to build
|
||||
include: ['lib/**'],
|
||||
comments: 'none',
|
||||
}),
|
||||
];
|
||||
|
||||
// IE11 does not support modules, so they are removed here, as well as in a
|
||||
// dedicated unit test build which is needed for the same reason.
|
||||
outputOptions.push({
|
||||
input: './lib/model-viewer-effects.js',
|
||||
output: {
|
||||
file: './dist/model-viewer-effects-umd.js',
|
||||
sourcemap: true,
|
||||
format: 'umd',
|
||||
name: 'ModelViewerEffects',
|
||||
globals: {
|
||||
three: 'three',
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
include: watchFiles,
|
||||
},
|
||||
external: ['three'],
|
||||
plugins: pluginsIE11,
|
||||
onwarn,
|
||||
});
|
||||
|
||||
plugins = [...plugins, terser()];
|
||||
|
||||
outputOptions.push({
|
||||
input: './dist/model-viewer-effects.js',
|
||||
output: {
|
||||
file: './dist/model-viewer-effects.min.js',
|
||||
sourcemap: true,
|
||||
format: 'esm',
|
||||
name: 'ModelViewerEffects',
|
||||
globals: {
|
||||
three: 'three',
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
include: watchFiles,
|
||||
},
|
||||
external: ['three'],
|
||||
plugins,
|
||||
onwarn,
|
||||
});
|
||||
|
||||
outputOptions.push({
|
||||
input: './dist/model-viewer-effects-umd.js',
|
||||
output: {
|
||||
file: './dist/model-viewer-effects-umd.min.js',
|
||||
sourcemap: true,
|
||||
format: 'umd',
|
||||
name: 'ModelViewerEffects',
|
||||
globals: {
|
||||
three: 'three',
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
include: watchFiles,
|
||||
},
|
||||
external: ['three'],
|
||||
plugins,
|
||||
onwarn,
|
||||
});
|
||||
|
||||
outputOptions.push({
|
||||
input: './lib/model-viewer-effects.d.ts',
|
||||
output: {
|
||||
file: './dist/model-viewer-effects.d.ts',
|
||||
format: 'esm',
|
||||
name: 'ModelViewerEffects',
|
||||
},
|
||||
plugins: [dts()],
|
||||
});
|
||||
}
|
||||
|
||||
export default outputOptions;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
@@ -0,0 +1,419 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {ModelViewerElement} from '@google/model-viewer';
|
||||
import {ModelScene} from '@google/model-viewer/lib/three-components/ModelScene.js';
|
||||
import {ReactiveElement} from 'lit';
|
||||
import {property} from 'lit/decorators.js';
|
||||
import {EffectComposer as PPEffectComposer, EffectPass, NormalPass, Pass, RenderPass, Selection} from 'postprocessing';
|
||||
import {Camera, HalfFloatType, NeutralToneMapping, ToneMapping, UnsignedByteType, WebGLRenderer} from 'three';
|
||||
|
||||
import {IMVEffect, IntegrationOptions, MVEffectBase} from './effects/mixins/effect-base.js';
|
||||
import {disposeEffectPass, isConvolution, validateLiteralType} from './utilities.js';
|
||||
|
||||
export const $scene = Symbol('scene');
|
||||
export const $composer = Symbol('composer');
|
||||
export const $modelViewerElement = Symbol('modelViewerElement');
|
||||
export const $effectComposer = Symbol('effectComposer');
|
||||
export const $renderPass = Symbol('renderPass');
|
||||
export const $normalPass = Symbol('normalPass');
|
||||
export const $effectPasses = Symbol('effectsPass');
|
||||
export const $requires = Symbol('requires');
|
||||
export const $effects = Symbol('effects');
|
||||
export const $selection = Symbol('selection');
|
||||
export const $onSceneLoad = Symbol('onSceneLoad');
|
||||
export const $resetEffectPasses = Symbol('resetEffectPasses');
|
||||
export const $userEffectCount = Symbol('userEffectCount');
|
||||
export const $tonemapping = Symbol('tonemapping');
|
||||
const $updateProperties = Symbol('updateProperties');
|
||||
|
||||
/**
|
||||
* Light wrapper around {@link EffectComposer} for storing the `scene` and
|
||||
* `camera at a top level, and setting them for every {@link Pass} added.
|
||||
*/
|
||||
export class EffectComposer extends PPEffectComposer {
|
||||
public camera?: Camera;
|
||||
public scene?: ModelScene;
|
||||
public dirtyRender?: boolean;
|
||||
|
||||
[$tonemapping]: ToneMapping = NeutralToneMapping;
|
||||
|
||||
constructor(renderer?: WebGLRenderer, options?: {
|
||||
depthBuffer?: boolean;
|
||||
stencilBuffer?: boolean;
|
||||
alpha?: boolean;
|
||||
multisampling?: number;
|
||||
frameBufferType?: number;
|
||||
}) {
|
||||
super(renderer, options);
|
||||
}
|
||||
|
||||
private preRender() {
|
||||
// the EffectComposer expects autoClear to be false so that buffers aren't
|
||||
// cleared between renders while the threeRenderer should be true so that
|
||||
// the frames are cleared each render.
|
||||
const renderer = this.getRenderer();
|
||||
renderer.autoClear = false;
|
||||
renderer.toneMapping = this[$tonemapping];
|
||||
}
|
||||
|
||||
private postRender() {
|
||||
const renderer = this.getRenderer();
|
||||
renderer.toneMapping = NeutralToneMapping;
|
||||
renderer.autoClear = true;
|
||||
}
|
||||
|
||||
override render(deltaTime?: number|undefined): void {
|
||||
this.preRender();
|
||||
super.render(deltaTime);
|
||||
this.postRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a pass, optionally at a specific index.
|
||||
* Additionally sets `scene` and `camera`.
|
||||
* @param pass A new pass.
|
||||
* @param index An index at which the pass should be inserted.
|
||||
*/
|
||||
override addPass(pass: Pass, index?: number): void {
|
||||
super.addPass(pass, index);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
override setMainCamera(camera: Camera): void {
|
||||
this.camera = camera;
|
||||
super.setMainCamera(camera);
|
||||
}
|
||||
|
||||
override setMainScene(scene: ModelScene): void {
|
||||
this.scene = scene;
|
||||
super.setMainScene(scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effect Materials that use the camera need to be manually updated whenever
|
||||
* the camera settings update.
|
||||
*/
|
||||
refresh(): void {
|
||||
if (this.camera && this.scene) {
|
||||
super.setMainCamera(this.camera);
|
||||
super.setMainScene(this.scene);
|
||||
}
|
||||
}
|
||||
|
||||
beforeRender(_time: DOMHighResTimeStamp, _delta: DOMHighResTimeStamp): void {
|
||||
if (this.dirtyRender) {
|
||||
this.scene?.queueRender();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type MVPass = Pass&IntegrationOptions;
|
||||
|
||||
export const RENDER_MODES = ['performance', 'quality'] as const;
|
||||
export type RenderMode = typeof RENDER_MODES[number];
|
||||
|
||||
const N_DEFAULT_PASSES = 2; // RenderPass, NormalPass
|
||||
|
||||
export class MVEffectComposer extends ReactiveElement {
|
||||
static get is() {
|
||||
return 'effect-composer';
|
||||
}
|
||||
|
||||
/**
|
||||
* `quality` | `performance`. Changing this after the element was constructed
|
||||
* has no effect.
|
||||
*
|
||||
* Using `quality` improves banding on certain effects, at a memory cost. Use
|
||||
* in HDR scenarios.
|
||||
*
|
||||
* `performance` should be sufficient for most use-cases.
|
||||
* @default 'performance'
|
||||
*/
|
||||
@property({type: String, attribute: 'render-mode'})
|
||||
renderMode: RenderMode = 'performance';
|
||||
|
||||
/**
|
||||
* Anti-Aliasing using the MSAA algorithm. Doesn't work well with depth-based
|
||||
* effects.
|
||||
*
|
||||
* Recommended to use with a factor of 2.
|
||||
* @default 0
|
||||
*/
|
||||
@property({type: Number, attribute: 'msaa'}) msaa: number = 0;
|
||||
|
||||
protected[$composer]?: EffectComposer;
|
||||
protected[$modelViewerElement]?: ModelViewerElement;
|
||||
protected[$renderPass]: RenderPass;
|
||||
protected[$normalPass]: NormalPass;
|
||||
protected[$selection]: Selection;
|
||||
protected[$userEffectCount]: number = 0;
|
||||
|
||||
get[$effectComposer]() {
|
||||
if (!this[$composer])
|
||||
throw new Error(
|
||||
'The EffectComposer has not been instantiated yet. Please make sure the component is properly mounted on the Document within a <model-viewer> element.');
|
||||
return this[$composer];
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of custom {@link MVPass}'s added with {@link addPass}.
|
||||
*/
|
||||
get userPasses(): MVPass[] {
|
||||
return this[$effectComposer].passes.slice(
|
||||
N_DEFAULT_PASSES, N_DEFAULT_PASSES + this[$userEffectCount]);
|
||||
}
|
||||
|
||||
get modelViewerElement() {
|
||||
if (!this[$modelViewerElement])
|
||||
throw new Error(
|
||||
'<effect-composer> must be a child of a <model-viewer> component.');
|
||||
return this[$modelViewerElement];
|
||||
}
|
||||
|
||||
/**
|
||||
* The Texture buffer of the inbuilt {@link NormalPass}.
|
||||
*/
|
||||
get normalBuffer() {
|
||||
return this[$normalPass].texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* A selection of all {@link Mesh}'s in the ModelScene.
|
||||
*/
|
||||
get selection() {
|
||||
return this[$selection];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MVEffectComposer element.
|
||||
*
|
||||
* @warning The EffectComposer instance is created only on connection with the
|
||||
* DOM, so that the renderMode is properly taken into account. Do not interact
|
||||
* with this class if it is not mounted to the DOM.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this[$renderPass] = new RenderPass();
|
||||
this[$normalPass] = new NormalPass();
|
||||
this[$selection] = new Selection();
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
if (this.parentNode?.nodeName.toLowerCase() === 'model-viewer') {
|
||||
this[$modelViewerElement] = this.parentNode as ModelViewerElement;
|
||||
}
|
||||
|
||||
try {
|
||||
validateLiteralType(RENDER_MODES, this.renderMode);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
(e as Error).message + '\nrenderMode defaulting to: performance');
|
||||
}
|
||||
this[$composer] = new EffectComposer(undefined, {
|
||||
stencilBuffer: true,
|
||||
multisampling: this.msaa,
|
||||
frameBufferType: this.renderMode === 'quality' ? HalfFloatType :
|
||||
UnsignedByteType,
|
||||
});
|
||||
this.modelViewerElement.registerEffectComposer(this[$effectComposer]);
|
||||
this[$effectComposer].addPass(this[$renderPass], 0);
|
||||
this[$effectComposer].addPass(this[$normalPass], 1);
|
||||
|
||||
this[$onSceneLoad]();
|
||||
this.modelViewerElement.addEventListener(
|
||||
'before-render', this[$onSceneLoad]);
|
||||
this.updateEffects();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback && super.disconnectedCallback();
|
||||
this.modelViewerElement.unregisterEffectComposer();
|
||||
this.modelViewerElement.removeEventListener(
|
||||
'before-render', this[$onSceneLoad]);
|
||||
this[$effectComposer].dispose();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string|number|symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('msaa')) {
|
||||
this[$effectComposer].multisampling = this.msaa;
|
||||
}
|
||||
if (changedProperties.has('renderMode') &&
|
||||
changedProperties.get('renderMode') !== undefined) {
|
||||
throw new Error('renderMode cannot be changed after startup.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom Pass that extends the {@link Pass} class.
|
||||
* All passes added through this method will be prepended before all other
|
||||
* web-component effects.
|
||||
*
|
||||
* This method automatically sets the `mainScene` and `mainCamera` of the
|
||||
* pass.
|
||||
* @param {Pass} pass Custom Pass to add. The camera and scene are set
|
||||
* automatically.
|
||||
* @param {boolean} requireNormals Whether any effect in this pass uses
|
||||
* the {@link normalBuffer}
|
||||
* @param {boolean} requireDirtyRender Enable this if the effect requires a
|
||||
* render frame every frame. Significant performance impact from enabling
|
||||
* this.
|
||||
*/
|
||||
addPass(pass: Pass, requireNormals?: boolean, requireDirtyRender?: boolean):
|
||||
void {
|
||||
(pass as MVPass).requireNormals = requireNormals;
|
||||
(pass as MVPass).requireDirtyRender = requireDirtyRender;
|
||||
this[$effectComposer].addPass(
|
||||
pass,
|
||||
this[$userEffectCount] +
|
||||
N_DEFAULT_PASSES); // push after current userPasses, before any
|
||||
// web-component effects.
|
||||
this[$userEffectCount]++;
|
||||
// Enable the normalPass and dirtyRendering if required by any effect.
|
||||
this[$updateProperties]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and optionally disposes of a previously added Pass.
|
||||
* @param pass Custom Pass to remove
|
||||
* @param {Boolean} dispose Disposes of the Pass properties and effects.
|
||||
* Default is `true`.
|
||||
*/
|
||||
removePass(pass: Pass, dispose: boolean = true): void {
|
||||
if (!this[$effectComposer].passes.includes(pass))
|
||||
throw new Error(`Pass ${pass.name} not found.`);
|
||||
this[$effectComposer].removePass(pass);
|
||||
if (dispose)
|
||||
pass.dispose();
|
||||
// Enable the normalPass and dirtyRendering if required by any effect.
|
||||
this[$updateProperties]();
|
||||
this[$userEffectCount]--;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all existing EffectPasses, adding any new `<model-viewer-effects>`
|
||||
* Effects in the order they were added, after any custom Passes added
|
||||
* with {@link addPass}.
|
||||
*
|
||||
* Runs automatically whenever a new Effect is added.
|
||||
*/
|
||||
updateEffects(): void {
|
||||
this[$resetEffectPasses]();
|
||||
|
||||
// Iterate over all effects (web-component), and combines as many as
|
||||
// possible. Convolution effects must sit on their own EffectPass. In order
|
||||
// to preserve the correct effect order, the convolution effects separate
|
||||
// all effects before and after into separate EffectPasses.
|
||||
const effects = this[$effects];
|
||||
let i = 0;
|
||||
while (i < effects.length) {
|
||||
const separateIndex = effects.slice(i).findIndex(
|
||||
(effect) => effect.requireSeparatePass || isConvolution(effect));
|
||||
if (separateIndex != 0) {
|
||||
const effectPass = new EffectPass(
|
||||
undefined,
|
||||
...effects.slice(
|
||||
i, separateIndex == -1 ? effects.length : separateIndex));
|
||||
this[$effectComposer].addPass(effectPass);
|
||||
}
|
||||
|
||||
if (separateIndex != -1) {
|
||||
const convolutionPass =
|
||||
new EffectPass(undefined, effects[i + separateIndex]);
|
||||
this[$effectComposer].addPass(convolutionPass);
|
||||
i += separateIndex + 1;
|
||||
} else {
|
||||
break; // A convolution was not found, the first Effect pass contains
|
||||
// all effects from i to effects.length
|
||||
}
|
||||
}
|
||||
|
||||
// Enable the normalPass and dirtyRendering if required by any effect.
|
||||
this[$updateProperties]();
|
||||
|
||||
this.queueRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a render-frame manually.
|
||||
*/
|
||||
queueRender(): void {
|
||||
this[$scene]?.queueRender();
|
||||
}
|
||||
|
||||
get[$scene]() {
|
||||
return this[$effectComposer].scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets child effects
|
||||
*/
|
||||
get[$effects](): IMVEffect[] {
|
||||
// iterate over all web-component children effects
|
||||
const effects: IMVEffect[] = [];
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
const childEffect = this.children.item(i) as MVEffectBase;
|
||||
if (!childEffect.effects)
|
||||
continue;
|
||||
const childEffects = childEffect.effects;
|
||||
if (childEffects) {
|
||||
effects.push(...childEffects.filter((effect) => !effect.disabled));
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets effectPasses of child effects
|
||||
*/
|
||||
get[$effectPasses]() {
|
||||
return this[$effectComposer].passes.slice(
|
||||
N_DEFAULT_PASSES + this[$userEffectCount]) as EffectPass[];
|
||||
}
|
||||
|
||||
[$onSceneLoad] = (): void => {
|
||||
this[$effectComposer].refresh();
|
||||
// Place all Geometries in the selection
|
||||
this[$selection].clear();
|
||||
this[$scene]?.traverse(
|
||||
(obj) => obj.hasOwnProperty('geometry') && this[$selection].add(obj));
|
||||
this.dispatchEvent(new CustomEvent('updated-selection'));
|
||||
};
|
||||
|
||||
[$updateProperties]() {
|
||||
this[$normalPass].enabled = this[$requires]('requireNormals');
|
||||
this[$normalPass].renderToScreen = false;
|
||||
this[$effectComposer].dirtyRender = this[$requires]('requireDirtyRender');
|
||||
this[$renderPass].renderToScreen =
|
||||
this[$effectComposer].passes.length === N_DEFAULT_PASSES;
|
||||
}
|
||||
|
||||
[$requires](property: 'requireNormals'|'requireSeparatePass'|
|
||||
'requireDirtyRender'): boolean {
|
||||
return this[$effectComposer].passes.some(
|
||||
(pass: any) => pass[property] ||
|
||||
(pass.effects &&
|
||||
pass.effects.some((effect: IMVEffect) => effect[property])));
|
||||
}
|
||||
|
||||
[$resetEffectPasses](): void {
|
||||
this[$effectPasses].forEach((pass) => {
|
||||
this[$effectComposer].removePass(pass);
|
||||
disposeEffectPass(pass);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { BlendFunction, BloomEffect } from 'postprocessing';
|
||||
import { $updateProperties, $effectOptions, MVEffectBase } from './mixins/effect-base.js';
|
||||
|
||||
export class MVBloomEffect extends MVEffectBase {
|
||||
static get is() {
|
||||
return 'bloom-effect';
|
||||
}
|
||||
|
||||
/**
|
||||
* The strength of the bloom effect.
|
||||
*/
|
||||
@property({ type: Number, attribute: 'strength', reflect: true })
|
||||
strength = 1;
|
||||
|
||||
/**
|
||||
* Value in the range of (0, 1). Pixels with a brightness above this will bloom.
|
||||
*/
|
||||
@property({ type: Number, attribute: 'threshold', reflect: true })
|
||||
threshold = 0.85;
|
||||
|
||||
/**
|
||||
* Value in the range of (0, 1).
|
||||
*/
|
||||
@property({ type: Number, attribute: 'radius', reflect: true })
|
||||
radius = 0.85;
|
||||
|
||||
/**
|
||||
* Value in the range of (0, 1).
|
||||
*/
|
||||
@property({ type: Number, attribute: 'smoothing', reflect: true })
|
||||
smoothing = 0.025;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.effects = [new BloomEffect(this[$effectOptions])];
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$updateProperties]();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has('strength') ||
|
||||
changedProperties.has('threshold') ||
|
||||
changedProperties.has('smoothing') ||
|
||||
changedProperties.has('radius')
|
||||
) {
|
||||
this[$updateProperties]();
|
||||
}
|
||||
}
|
||||
|
||||
[$updateProperties](): void {
|
||||
(this.effects[0] as BloomEffect).luminanceMaterial.threshold = this.threshold;
|
||||
(this.effects[0] as BloomEffect).luminanceMaterial.smoothing = this.smoothing;
|
||||
(this.effects[0] as BloomEffect).intensity = this.strength;
|
||||
(this.effects[0] as any).mipmapBlurPass.radius = this.radius;
|
||||
this.effectComposer.queueRender();
|
||||
}
|
||||
|
||||
get [$effectOptions]() {
|
||||
return {
|
||||
blendFunction: BlendFunction.ADD,
|
||||
mipmapBlur: true,
|
||||
radius: this.radius,
|
||||
luminanceThreshold: this.threshold,
|
||||
luminanceSmoothing: this.smoothing,
|
||||
intensity: this.strength,
|
||||
} as ConstructorParameters<typeof BloomEffect>[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {property} from 'lit/decorators.js';
|
||||
import {BlendFunction, BrightnessContrastEffect, HueSaturationEffect, ToneMappingEffect, ToneMappingMode as PPToneMappingMode} from 'postprocessing';
|
||||
import {NeutralToneMapping, NoToneMapping} from 'three';
|
||||
|
||||
import {$effectComposer, $tonemapping} from '../effect-composer.js';
|
||||
import {clamp, validateLiteralType, wrapClamp} from '../utilities.js';
|
||||
|
||||
import {$updateProperties, MVEffectBase} from './mixins/effect-base.js';
|
||||
|
||||
const TWO_PI = Math.PI * 2;
|
||||
|
||||
export type ToneMappingMode = keyof typeof PPToneMappingMode;
|
||||
;
|
||||
export const TONEMAPPING_MODES =
|
||||
Object.keys(PPToneMappingMode) as ToneMappingMode[];
|
||||
|
||||
export class MVColorGradeEffect extends MVEffectBase {
|
||||
static get is() {
|
||||
return 'color-grade-effect';
|
||||
}
|
||||
|
||||
/**
|
||||
* `reinhard | reinhard2 | reinhard_adaptive | optimized_cineon | aces_filmic
|
||||
* | linear`
|
||||
* @default 'aces_filmic'
|
||||
*/
|
||||
@property({type: String, attribute: 'tonemapping', reflect: true})
|
||||
tonemapping: ToneMappingMode = 'ACES_FILMIC'
|
||||
|
||||
/**
|
||||
* Value in the range of (-1, 1).
|
||||
*/
|
||||
@property({type: Number, attribute: 'brightness', reflect: true})
|
||||
brightness = 0;
|
||||
|
||||
/**
|
||||
* Value in the range of (-1, 1).
|
||||
*/
|
||||
@property({type: Number, attribute: 'contrast', reflect: true}) contrast = 0;
|
||||
|
||||
/**
|
||||
* Value in the range of (-1, 1).
|
||||
*/
|
||||
@property({type: Number, attribute: 'saturation', reflect: true})
|
||||
saturation = 0;
|
||||
|
||||
/**
|
||||
* Value in the range of (0, 2 * PI).
|
||||
*
|
||||
* This property is wrapping, meaning that if you set it above the max it
|
||||
* resets to the minimum and vice-versa.
|
||||
*/
|
||||
@property({type: Number, attribute: 'hue', reflect: true}) hue = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.effects = [
|
||||
new ToneMappingEffect({
|
||||
mode: PPToneMappingMode.ACES_FILMIC,
|
||||
}),
|
||||
new HueSaturationEffect({
|
||||
hue: wrapClamp(this.hue, 0, TWO_PI),
|
||||
saturation: clamp(this.saturation, -1, 1),
|
||||
blendFunction: BlendFunction.SRC,
|
||||
}),
|
||||
new BrightnessContrastEffect({
|
||||
brightness: clamp(this.brightness, -1, 1),
|
||||
contrast: clamp(this.contrast, -1, 1),
|
||||
blendFunction: BlendFunction.SRC,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$updateProperties]();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string|number|symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('tonemapping') ||
|
||||
changedProperties.has('brightness') ||
|
||||
changedProperties.has('contrast') || changedProperties.has('hue') ||
|
||||
changedProperties.has('saturation') ||
|
||||
changedProperties.has('blendMode')) {
|
||||
this[$updateProperties]();
|
||||
}
|
||||
}
|
||||
|
||||
[$updateProperties]() {
|
||||
if (this.blendMode === 'SKIP') {
|
||||
this.effectComposer[$effectComposer][$tonemapping] = NeutralToneMapping;
|
||||
} else {
|
||||
this.effectComposer[$effectComposer][$tonemapping] = NoToneMapping;
|
||||
}
|
||||
this.saturation = clamp(this.saturation, -1, 1);
|
||||
this.hue = wrapClamp(this.hue, 0, TWO_PI);
|
||||
this.brightness = clamp(this.brightness, -1, 1);
|
||||
this.contrast = clamp(this.contrast, -1, 1);
|
||||
(this.effects[1] as HueSaturationEffect).saturation = this.saturation;
|
||||
(this.effects[1] as HueSaturationEffect).hue = this.hue;
|
||||
(this.effects[2] as BrightnessContrastEffect).brightness = this.brightness;
|
||||
(this.effects[2] as BrightnessContrastEffect).contrast = this.contrast;
|
||||
try {
|
||||
this.tonemapping = this.tonemapping.toUpperCase() as ToneMappingMode;
|
||||
validateLiteralType(TONEMAPPING_MODES, this.tonemapping);
|
||||
(this.effects[0] as ToneMappingEffect).mode =
|
||||
PPToneMappingMode[this.tonemapping];
|
||||
} finally {
|
||||
this.effectComposer.queueRender();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { ChromaticAberrationEffect, GlitchEffect, GlitchMode as Mode } from 'postprocessing';
|
||||
import { Vector2 } from 'three';
|
||||
import { clamp, validateLiteralType } from '../utilities.js';
|
||||
import { $updateProperties, $effectOptions, MVEffectBase } from './mixins/effect-base.js';
|
||||
|
||||
|
||||
export const GLITCH_MODES = ['sporadic', 'constant'] as const;
|
||||
export type GlitchMode = typeof GLITCH_MODES[number];
|
||||
|
||||
export class MVGlitchEffect extends MVEffectBase {
|
||||
static get is() {
|
||||
return 'glitch-effect';
|
||||
}
|
||||
|
||||
/**
|
||||
* Value in the range of (0, 1).
|
||||
*/
|
||||
@property({ type: Number, attribute: 'strength', reflect: true })
|
||||
strength: number = 0.5;
|
||||
|
||||
/**
|
||||
* `sporadic` | `constant`
|
||||
* @default 'sporadic'
|
||||
*/
|
||||
@property({ type: String, attribute: 'mode', reflect: true })
|
||||
mode: GlitchMode = 'sporadic';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const chromaticAberrationEffect = new ChromaticAberrationEffect();
|
||||
const glitchEffect = new GlitchEffect(this[$effectOptions](chromaticAberrationEffect));
|
||||
this.effects = [glitchEffect, chromaticAberrationEffect];
|
||||
this.effects[1].requireDirtyRender = true;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$updateProperties]();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('mode') || changedProperties.has('strength')) {
|
||||
this[$updateProperties]();
|
||||
}
|
||||
}
|
||||
|
||||
[$updateProperties](): void {
|
||||
this.strength = clamp(this.strength, 0, 1);
|
||||
this.mode = this.mode.toLowerCase() as GlitchMode;
|
||||
try {
|
||||
validateLiteralType(GLITCH_MODES, this.mode);
|
||||
} catch(e) {
|
||||
console.error((e as Error).message + + "\nmode defaulting to 'sporadic'")
|
||||
}
|
||||
if (this.strength == 0) {
|
||||
(this.effects[0] as GlitchEffect).columns = 0;
|
||||
(this.effects[0] as GlitchEffect).mode = this.mode === 'constant' ? Mode.CONSTANT_MILD : Mode.SPORADIC;
|
||||
} else {
|
||||
(this.effects[0] as GlitchEffect).columns = 0.06;
|
||||
(this.effects[0] as GlitchEffect).mode = this.mode === 'constant' ? Mode.CONSTANT_WILD : Mode.SPORADIC;
|
||||
}
|
||||
(this.effects[0] as GlitchEffect).maxStrength = this.strength;
|
||||
(this.effects[0] as GlitchEffect).ratio = 1 - this.strength;
|
||||
}
|
||||
|
||||
[$effectOptions](chromaticAberrationEffect: ChromaticAberrationEffect) {
|
||||
this.strength = clamp(this.strength, 0, 1);
|
||||
return {
|
||||
chromaticAberrationOffset: chromaticAberrationEffect.offset,
|
||||
delay: new Vector2(1 * 1000, 3.5 * 1000),
|
||||
duration: new Vector2(0.5 * 1000, 1 * 1000),
|
||||
strength: new Vector2(0.075, this.strength),
|
||||
ratio: 1 - this.strength,
|
||||
} as ConstructorParameters<typeof GlitchEffect>[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReactiveElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { BlendFunction } from 'postprocessing';
|
||||
import { Constructor, clampNormal, validateLiteralType } from '../../utilities.js';
|
||||
import { IEffectBaseMixin } from './effect-base.js';
|
||||
|
||||
export const $setDefaultProperties = Symbol('setDefaultProperties');
|
||||
|
||||
export type BlendMode = keyof typeof BlendFunction;
|
||||
export const BLEND_MODES = Object.keys(BlendFunction) as BlendMode[];
|
||||
|
||||
export interface IBlendModeMixin {
|
||||
opacity: number;
|
||||
blendMode: BlendMode;
|
||||
[$setDefaultProperties](): void;
|
||||
}
|
||||
|
||||
export const BlendModeMixin = <T extends Constructor<IEffectBaseMixin & ReactiveElement>>(
|
||||
EffectClass: T
|
||||
): Constructor<IBlendModeMixin> & T => {
|
||||
class BlendEffectElement extends EffectClass {
|
||||
/**
|
||||
* The function to use to blend the effect with the base render.
|
||||
*/
|
||||
@property({ type: String, attribute: 'blend-mode', reflect: true })
|
||||
blendMode: 'DEFAULT' | BlendMode = 'DEFAULT';
|
||||
|
||||
/**
|
||||
* The opacity of the effect that will be blended with the base render.
|
||||
*/
|
||||
@property({ type: Number, attribute: 'opacity', reflect: true })
|
||||
opacity: number = 1;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$setDefaultProperties]();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('blendMode') || changedProperties.has('opacity')) {
|
||||
this.opacity = clampNormal(this.opacity);
|
||||
this.blendMode = this.blendMode.toUpperCase() as BlendMode;
|
||||
this.effects.forEach((effect) => {
|
||||
if (this.blendMode === 'DEFAULT') {
|
||||
if (effect.blendMode.defaultBlendFunction === undefined) throw new Error(`${effect.name} has no default blend function`);
|
||||
effect.blendMode.blendFunction = effect.blendMode.defaultBlendFunction;
|
||||
} else {
|
||||
validateLiteralType(BLEND_MODES, this.blendMode);
|
||||
effect.blendMode.blendFunction = BlendFunction[this.blendMode];
|
||||
}
|
||||
effect.disabled = this.blendMode === 'SKIP';
|
||||
effect.blendMode.setOpacity(this.opacity);
|
||||
});
|
||||
// Recreate EffectPasses if the new or old value was 'skip'
|
||||
if (this.blendMode === 'SKIP' || changedProperties.get('blendMode') === 'SKIP') {
|
||||
this.effectComposer.updateEffects();
|
||||
}
|
||||
this.effectComposer.queueRender();
|
||||
}
|
||||
}
|
||||
|
||||
protected [$setDefaultProperties]() {
|
||||
this.effects.forEach((effect) => {
|
||||
effect.blendMode.defaultBlendFunction = effect.blendMode.blendFunction;
|
||||
});
|
||||
}
|
||||
}
|
||||
return BlendEffectElement as Constructor<IBlendModeMixin> & T;
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { LitElement, ReactiveElement } from 'lit';
|
||||
import { BlendFunction, BlendMode, Effect } from 'postprocessing';
|
||||
import { $effectComposer, MVEffectComposer } from '../../effect-composer.js';
|
||||
import { Constructor } from '../../utilities.js';
|
||||
import { BlendModeMixin } from './blend-mode.js';
|
||||
import { getComponentName } from '../utilities.js';
|
||||
|
||||
export const $updateProperties = Symbol('updateProperties');
|
||||
export const $effectOptions = Symbol('effectOptions');
|
||||
|
||||
export interface IMVBlendMode extends BlendMode {
|
||||
defaultBlendFunction?: BlendFunction;
|
||||
}
|
||||
|
||||
export interface IntegrationOptions {
|
||||
/**
|
||||
* Enable this if effect uses the built-in {@link NormalPass}
|
||||
*/
|
||||
requireNormals?: boolean;
|
||||
/**
|
||||
* Enable this if the effect requires a render frame every frame.
|
||||
* @warning Significant performance impact from enabling this
|
||||
*/
|
||||
requireDirtyRender?: boolean;
|
||||
}
|
||||
|
||||
export interface IMVEffect extends Effect, IntegrationOptions {
|
||||
readonly blendMode: IMVBlendMode;
|
||||
/**
|
||||
* Enable this if the effect doesn't play well when used with other effects.
|
||||
*/
|
||||
requireSeparatePass?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IEffectBaseMixin {
|
||||
effects: IMVEffect[];
|
||||
effectComposer: MVEffectComposer;
|
||||
}
|
||||
|
||||
export const EffectBaseMixin = <T extends Constructor<ReactiveElement>>(EffectClass: T): Constructor<IEffectBaseMixin> & T => {
|
||||
class EffectBaseElement extends EffectClass {
|
||||
[$effectComposer]?: MVEffectComposer;
|
||||
protected effects!: IMVEffect[];
|
||||
|
||||
/**
|
||||
* The parent {@link MVEffectComposer} element.
|
||||
*/
|
||||
protected get effectComposer() {
|
||||
if (!this[$effectComposer]) throw new Error(`${getComponentName(this as any)} must be a child of a <model-viewer> component.`);
|
||||
return this[$effectComposer];
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
if (this.parentNode?.nodeName.toLowerCase() === 'effect-composer') {
|
||||
this[$effectComposer] = this.parentNode as MVEffectComposer;
|
||||
}
|
||||
this.effectComposer.updateEffects();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback && super.disconnectedCallback();
|
||||
this.effects.forEach((effect) => effect.dispose());
|
||||
this.effectComposer.updateEffects();
|
||||
}
|
||||
}
|
||||
return EffectBaseElement as Constructor<IEffectBaseMixin> & T;
|
||||
};
|
||||
|
||||
export const MVEffectBase = BlendModeMixin(EffectBaseMixin(LitElement));
|
||||
export type MVEffectBase = InstanceType<typeof MVEffectBase>;
|
||||
@@ -0,0 +1,80 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReactiveElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { Selection } from 'postprocessing';
|
||||
import { $selection, $scene } from '../../effect-composer.js';
|
||||
import { Constructor } from '../../utilities.js';
|
||||
import { IEffectBaseMixin, IMVEffect } from './effect-base.js';
|
||||
import { Object3D } from 'three';
|
||||
|
||||
export const $setSelection = Symbol('setSelection');
|
||||
|
||||
export interface ISelectionEffect extends IMVEffect {
|
||||
selection?: Selection;
|
||||
}
|
||||
|
||||
export interface ISelectiveMixin {
|
||||
selection: Array<string | Object3D>;
|
||||
}
|
||||
|
||||
export const SelectiveMixin = <T extends Constructor<IEffectBaseMixin & ReactiveElement>>(
|
||||
EffectClass: T
|
||||
): Constructor<ISelectiveMixin> & T => {
|
||||
class SelectiveEffectElement extends EffectClass {
|
||||
/**
|
||||
* The objects to attemp to place into the effect selection. Can be either the 'name' or the actual objects themselves.
|
||||
*
|
||||
* Note that since this is an array property, it must be set using the '=' operator in order to properly update.
|
||||
*/
|
||||
@property({ type: Array })
|
||||
selection: Array<string | Object3D> = [];
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$setSelection]();
|
||||
this.effectComposer.addEventListener('updated-selection', this[$setSelection]);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback && super.disconnectedCallback();
|
||||
this.effectComposer.removeEventListener('updated-selection', this[$setSelection]);
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('selection')) {
|
||||
this[$setSelection]();
|
||||
this.effectComposer.queueRender();
|
||||
}
|
||||
}
|
||||
|
||||
[$setSelection] = () => {
|
||||
const { effectComposer } = this;
|
||||
if (!effectComposer) return;
|
||||
|
||||
if (this.selection?.length > 0) {
|
||||
const selection: Object3D[] = [];
|
||||
const scene = effectComposer[$scene];
|
||||
scene?.traverse((obj) => (this.selection.includes(obj.name) || this.selection.includes(obj)) && selection.push(obj));
|
||||
this.effects.forEach((effect: ISelectionEffect) => effect.selection?.set(selection));
|
||||
} else {
|
||||
this.effects.forEach((effect: ISelectionEffect) => effect.selection?.set(effectComposer[$selection].values()));
|
||||
}
|
||||
};
|
||||
}
|
||||
return SelectiveEffectElement as Constructor<ISelectiveMixin> & T;
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { BlendFunction, OutlineEffect } from 'postprocessing';
|
||||
import { Color, ColorRepresentation } from 'three';
|
||||
import { $updateProperties, $effectOptions, MVEffectBase } from './mixins/effect-base.js';
|
||||
import { SelectiveMixin } from './mixins/selective.js';
|
||||
import { getKernelSize, TEMP_CAMERA } from './utilities.js';
|
||||
|
||||
export class MVOutlineEffect extends SelectiveMixin(MVEffectBase) {
|
||||
static get is() {
|
||||
return 'outline-effect';
|
||||
}
|
||||
|
||||
/**
|
||||
* String or RGB #-hexadecimal Color.
|
||||
* @default 'white'
|
||||
*/
|
||||
@property({ type: String || Number, attribute: 'color', reflect: true })
|
||||
color: ColorRepresentation = 'white';
|
||||
|
||||
/**
|
||||
* A larger value denotes a thicker edge.
|
||||
* @default 1
|
||||
*/
|
||||
@property({ type: Number, attribute: 'strength', reflect: true })
|
||||
strength = 1;
|
||||
|
||||
/**
|
||||
* Value in the range of (0, 6). Controls the edge blur strength.
|
||||
* @default 1
|
||||
*/
|
||||
@property({ type: Number, attribute: 'smoothing', reflect: true })
|
||||
smoothing = 1;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.effects = [new OutlineEffect(undefined, TEMP_CAMERA, this[$effectOptions])];
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$updateProperties]();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('color') || changedProperties.has('smoothing') || changedProperties.has('strength')) {
|
||||
this[$updateProperties]();
|
||||
}
|
||||
}
|
||||
|
||||
[$updateProperties]() {
|
||||
(this.effects[0] as OutlineEffect).edgeStrength = this.strength;
|
||||
(this.effects[0] as OutlineEffect).visibleEdgeColor = new Color(this.color);
|
||||
(this.effects[0] as OutlineEffect).hiddenEdgeColor = new Color(this.color);
|
||||
(this.effects[0] as OutlineEffect).blurPass.enabled = Math.round(this.smoothing) > 0;
|
||||
(this.effects[0] as OutlineEffect).blurPass.kernelSize = getKernelSize(this.smoothing);
|
||||
this.effectComposer.queueRender();
|
||||
}
|
||||
|
||||
get [$effectOptions]() {
|
||||
return {
|
||||
blendFunction: BlendFunction.SCREEN,
|
||||
edgeStrength: this.strength,
|
||||
pulseSpeed: 0.0,
|
||||
visibleEdgeColor: new Color(this.color).getHex(),
|
||||
hiddenEdgeColor: new Color(this.color).getHex(),
|
||||
blur: Math.round(this.smoothing) > 0,
|
||||
kernelSize: getKernelSize(this.smoothing),
|
||||
xRay: true,
|
||||
resolutionScale: 1,
|
||||
} as ConstructorParameters<typeof OutlineEffect>[2];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { PixelationEffect } from 'postprocessing';
|
||||
import { $updateProperties, MVEffectBase } from './mixins/effect-base.js';
|
||||
|
||||
export class MVPixelateEffect extends MVEffectBase {
|
||||
static get is() {
|
||||
return 'pixelate-effect';
|
||||
}
|
||||
|
||||
/**
|
||||
* The pixel granularity. Higher value = lower resolution.
|
||||
* @default 10
|
||||
*/
|
||||
@property({ type: Number, attribute: 'granularity', reflect: true })
|
||||
granularity = 10.0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.effects = [new PixelationEffect(this.granularity)];
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$updateProperties]();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('granularity')) {
|
||||
this[$updateProperties]();
|
||||
}
|
||||
}
|
||||
|
||||
[$updateProperties]() {
|
||||
(this.effects[0] as PixelationEffect).granularity = this.granularity;
|
||||
this.effectComposer.queueRender();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { BlendFunction, SelectiveBloomEffect } from 'postprocessing';
|
||||
import { $updateProperties, $effectOptions, MVEffectBase } from './mixins/effect-base.js';
|
||||
import { SelectiveMixin } from './mixins/selective.js';
|
||||
import { TEMP_CAMERA } from './utilities.js';
|
||||
|
||||
export class MVSelectiveBloomEffect extends SelectiveMixin(MVEffectBase) {
|
||||
static get is() {
|
||||
return 'selective-bloom-effect';
|
||||
}
|
||||
|
||||
/**
|
||||
* The strength of the bloom effect.
|
||||
*/
|
||||
@property({ type: Number, attribute: 'strength', reflect: true })
|
||||
strength = 1;
|
||||
|
||||
/**
|
||||
* Value in the range of (0, 1). Pixels with a brightness above this will bloom.
|
||||
*/
|
||||
@property({ type: Number, attribute: 'threshold', reflect: true })
|
||||
threshold = 0.85;
|
||||
|
||||
/**
|
||||
* Value in the range of (0, 1).
|
||||
*/
|
||||
@property({ type: Number, attribute: 'smoothing', reflect: true })
|
||||
smoothing = 0.025;
|
||||
|
||||
/**
|
||||
* Value in the range of (0, 1).
|
||||
*/
|
||||
@property({ type: Number, attribute: 'radius', reflect: true })
|
||||
radius = 0.85;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.effects = [new SelectiveBloomEffect(undefined, TEMP_CAMERA, this[$effectOptions])];
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$updateProperties]();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has('strength') ||
|
||||
changedProperties.has('threshold') ||
|
||||
changedProperties.has('smoothing') ||
|
||||
changedProperties.has('radius')
|
||||
) {
|
||||
this[$updateProperties]();
|
||||
}
|
||||
}
|
||||
|
||||
[$updateProperties](): void {
|
||||
(this.effects[0] as SelectiveBloomEffect).luminanceMaterial.threshold = this.threshold;
|
||||
(this.effects[0] as SelectiveBloomEffect).luminanceMaterial.smoothing = this.smoothing;
|
||||
(this.effects[0] as SelectiveBloomEffect).intensity = this.strength;
|
||||
(this.effects[0] as any).mipmapBlurPass.radius = this.radius;
|
||||
this.effectComposer.queueRender();
|
||||
}
|
||||
|
||||
get [$effectOptions]() {
|
||||
return {
|
||||
blendFunction: BlendFunction.ADD,
|
||||
mipmapBlur: true,
|
||||
radius: this.radius,
|
||||
luminanceThreshold: this.threshold,
|
||||
luminanceSmoothing: this.smoothing,
|
||||
intensity: this.strength,
|
||||
} as ConstructorParameters<typeof SelectiveBloomEffect>[2];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { SMAAEffect, SMAAPreset } from 'postprocessing';
|
||||
import { $updateProperties, MVEffectBase } from './mixins/effect-base.js';
|
||||
import { validateLiteralType } from '../utilities.js';
|
||||
|
||||
export type SMAAQuality = keyof typeof SMAAPreset;
|
||||
export const SMAA_QUALITIES = Object.keys(SMAAPreset) as SMAAQuality[];
|
||||
|
||||
export class MVSMAAEffect extends MVEffectBase {
|
||||
static get is() {
|
||||
return 'smaa-effect';
|
||||
}
|
||||
|
||||
/**
|
||||
* `low | medium | high | ultra`
|
||||
* @default 'medium'
|
||||
*/
|
||||
@property({ type: String, attribute: 'quality', reflect: true })
|
||||
quality: SMAAQuality = 'MEDIUM';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.effects = [new SMAAEffect({ preset: SMAAPreset[this.quality] })];
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$updateProperties]();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('quality')) {
|
||||
this[$updateProperties]();
|
||||
}
|
||||
}
|
||||
|
||||
[$updateProperties]() {
|
||||
this.quality = this.quality.toUpperCase() as SMAAQuality;
|
||||
validateLiteralType(SMAA_QUALITIES, this.quality);
|
||||
(this.effects[0] as SMAAEffect).applyPreset(SMAAPreset[this.quality]);
|
||||
this.effectComposer.queueRender();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SSAOEffect } from 'postprocessing';
|
||||
import { $updateProperties, $effectOptions, MVEffectBase } from './mixins/effect-base.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { TEMP_CAMERA } from './utilities.js';
|
||||
import { $setDefaultProperties } from './mixins/blend-mode.js';
|
||||
|
||||
export class MVSSAOEffect extends MVEffectBase {
|
||||
static get is() {
|
||||
return 'ssao-effect';
|
||||
}
|
||||
|
||||
/**
|
||||
* The strength of the shadow occlusions. Higher value means darker shadows.
|
||||
*/
|
||||
@property({ type: Number, attribute: 'strength', reflect: true })
|
||||
strength: number = 2;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.effects = [new SSAOEffect(TEMP_CAMERA, undefined, this[$effectOptions])];
|
||||
this.effects[0].requireNormals = true;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback && super.connectedCallback();
|
||||
this[$setDefaultProperties]();
|
||||
this[$updateProperties]();
|
||||
}
|
||||
|
||||
update(changedProperties: Map<string | number | symbol, any>): void {
|
||||
super.update && super.update(changedProperties);
|
||||
if (changedProperties.has('strength')) {
|
||||
this[$updateProperties]();
|
||||
}
|
||||
}
|
||||
|
||||
[$updateProperties](): void {
|
||||
(this.effects[0] as SSAOEffect).intensity = this.strength;
|
||||
this.effectComposer.queueRender();
|
||||
}
|
||||
|
||||
[$setDefaultProperties]() {
|
||||
super[$setDefaultProperties]();
|
||||
(this.effects[0] as SSAOEffect).normalBuffer = this.effectComposer.normalBuffer;
|
||||
}
|
||||
|
||||
get [$effectOptions]() {
|
||||
return {
|
||||
worldDistanceThreshold: 1000,
|
||||
worldDistanceFalloff: 1000,
|
||||
worldProximityThreshold: 1000,
|
||||
worldProximityFalloff: 1000,
|
||||
luminanceInfluence: 0.7,
|
||||
samples: 16,
|
||||
fade: 0.05,
|
||||
radius: 0.05,
|
||||
intensity: this.strength,
|
||||
} as ConstructorParameters<typeof SSAOEffect>[2];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { KernelSize } from 'postprocessing';
|
||||
import { PerspectiveCamera } from 'three';
|
||||
import { clamp } from '../utilities.js';
|
||||
import { MVEffectBase } from './mixins/effect-base.js';
|
||||
|
||||
/**
|
||||
* Helper function for calculating the Kernel Size
|
||||
* @param n Range(0, 6)
|
||||
* @returns The relative Kernel Size
|
||||
*/
|
||||
export function getKernelSize(n: number): number {
|
||||
return Math.round(clamp(n + 1, KernelSize.VERY_SMALL, KernelSize.HUGE + 1)) - 1;
|
||||
}
|
||||
|
||||
export function getComponentName(obj: MVEffectBase): string {
|
||||
return '<' + obj.constructor.name.replace('MV', '').split(/(?=[A-Z])/).join('-').toLowerCase() + '>';
|
||||
}
|
||||
|
||||
// Used for effects which require a valid Camera for shader instance
|
||||
export const TEMP_CAMERA = new PerspectiveCamera();
|
||||
@@ -0,0 +1,64 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MVBloomEffect } from './effects/bloom.js';
|
||||
import { MVColorGradeEffect } from './effects/color-grade.js';
|
||||
import { MVGlitchEffect } from './effects/glitch.js';
|
||||
import { MVOutlineEffect } from './effects/outline.js';
|
||||
import { MVPixelateEffect } from './effects/pixelate.js';
|
||||
import { MVSMAAEffect } from './effects/smaa.js';
|
||||
import { MVSSAOEffect } from './effects/ssao.js';
|
||||
import { MVEffectComposer } from './effect-composer.js';
|
||||
import { MVEffectBase } from './effects/mixins/effect-base.js';
|
||||
import { SelectiveMixin } from './effects/mixins/selective.js';
|
||||
import { MVSelectiveBloomEffect } from './effects/selective-bloom.js';
|
||||
|
||||
customElements.define('effect-composer', MVEffectComposer);
|
||||
customElements.define('pixelate-effect', MVPixelateEffect);
|
||||
customElements.define('bloom-effect', MVBloomEffect);
|
||||
customElements.define('selective-bloom-effect', MVSelectiveBloomEffect);
|
||||
customElements.define('color-grade-effect', MVColorGradeEffect);
|
||||
customElements.define('outline-effect', MVOutlineEffect);
|
||||
customElements.define('smaa-effect', MVSMAAEffect);
|
||||
customElements.define('ssao-effect', MVSSAOEffect);
|
||||
customElements.define('glitch-effect', MVGlitchEffect);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'effect-composer': MVEffectComposer;
|
||||
'pixelate-effect': MVPixelateEffect;
|
||||
'bloom-effect': MVBloomEffect;
|
||||
'selective-bloom-effect': MVSelectiveBloomEffect;
|
||||
'color-grade-effect': MVColorGradeEffect;
|
||||
'outline-effect': MVOutlineEffect;
|
||||
'ssao-effect': MVSSAOEffect;
|
||||
'smaa-effect': MVSMAAEffect;
|
||||
'glitch-effect': MVGlitchEffect;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
MVEffectComposer as EffectComposer,
|
||||
MVPixelateEffect as PixelateEffect,
|
||||
MVBloomEffect as BloomEffect,
|
||||
MVSelectiveBloomEffect as SelectiveBloomEffect,
|
||||
MVColorGradeEffect as ColorGradeEffect,
|
||||
MVOutlineEffect as OutlineEffect,
|
||||
MVSSAOEffect as SSAOEffect,
|
||||
MVSMAAEffect as SMAAEffect,
|
||||
MVGlitchEffect as GlitchEffect,
|
||||
MVEffectBase as EffectBase,
|
||||
SelectiveMixin,
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {ModelViewerElement} from '@google/model-viewer';
|
||||
import {expect} from 'chai';
|
||||
import {DotScreenEffect, Effect, EffectPass, GridEffect} from 'postprocessing';
|
||||
import {Camera} from 'three';
|
||||
|
||||
import {$effectComposer, $normalPass, $renderPass, $scene} from '../effect-composer.js';
|
||||
import {EffectComposer} from '../model-viewer-effects.js';
|
||||
|
||||
import {assetPath, createModelViewerElement, waitForEvent} from './utilities.js';
|
||||
|
||||
suite('MVEffectComposer', () => {
|
||||
let element: ModelViewerElement;
|
||||
let composer: EffectComposer;
|
||||
|
||||
setup(async () => {
|
||||
element = createModelViewerElement(assetPath('models/Astronaut.glb'));
|
||||
composer = new EffectComposer();
|
||||
element.insertBefore(composer, element.firstChild);
|
||||
await waitForEvent(element, 'before-render');
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
suite('registered successfully', () => {
|
||||
suite('scene+camera', () => {
|
||||
test('has scene', () => {
|
||||
expect(composer[$scene]).to.be.ok;
|
||||
});
|
||||
|
||||
test('has camera', () => {
|
||||
expect(composer[$scene]).to.be.ok;
|
||||
});
|
||||
});
|
||||
suite('passes, selection', () => {
|
||||
test('renderPass + normalPass added successfuly', () => {
|
||||
expect(composer[$renderPass]).to.be.ok;
|
||||
expect(composer[$normalPass]).to.be.ok;
|
||||
expect(composer[$effectComposer].passes.length).to.eq(2);
|
||||
expect(composer[$effectComposer].passes[0])
|
||||
.to.eq(composer[$renderPass]);
|
||||
expect(composer[$effectComposer].passes[1])
|
||||
.to.eq(composer[$normalPass]);
|
||||
expect(composer[$normalPass].enabled).to.be.false;
|
||||
expect(composer[$normalPass].renderToScreen).to.be.false;
|
||||
expect((composer[$renderPass] as any).scene).to.eq(composer[$scene]);
|
||||
});
|
||||
|
||||
test('selection finds Meshes', () => {
|
||||
expect(composer.selection.size).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('userEffects', () => {
|
||||
let pass: EffectPass;
|
||||
let effects: Effect[] = [];
|
||||
test('adds grid effect', () => {
|
||||
const effect = new GridEffect();
|
||||
effects.push(effect);
|
||||
pass = new EffectPass(composer[$scene]?.camera as Camera, ...effects);
|
||||
composer.addPass(pass);
|
||||
expect(composer[$effectComposer].passes.length).to.eq(3);
|
||||
expect(composer[$effectComposer].passes[2]).to.eq(pass);
|
||||
expect((composer[$effectComposer].passes[2] as any).effects)
|
||||
.to.contain(effect);
|
||||
|
||||
composer.removePass(pass, false);
|
||||
});
|
||||
|
||||
test('multiple effects all on one layer', async () => {
|
||||
const effect = new DotScreenEffect();
|
||||
effects.push(effect);
|
||||
pass = new EffectPass(composer[$scene]?.camera as Camera, ...effects);
|
||||
composer.addPass(pass);
|
||||
expect(composer[$effectComposer].passes.length).to.eq(3);
|
||||
expect(composer[$effectComposer].passes[2]).to.eq(pass);
|
||||
expect((composer[$effectComposer].passes[2] as any).effects.length)
|
||||
.to.eq(2);
|
||||
expect((composer[$effectComposer].passes[2] as any).effects)
|
||||
.to.contain(effect);
|
||||
|
||||
composer.removePass(pass, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {ModelViewerElement} from '@google/model-viewer';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {ColorGradeEffect, EffectComposer} from '../../model-viewer-effects.js';
|
||||
import {ArraysAreEqual, assetPath, AverageHSL, CompareArrays, createModelViewerElement, rafPasses, screenshot, waitForEvent} from '../utilities.js';
|
||||
|
||||
suite('Color Grade Effect', () => {
|
||||
let element: ModelViewerElement;
|
||||
let composer: EffectComposer;
|
||||
let baseScreenshot: Uint8Array;
|
||||
let colorGrade: ColorGradeEffect;
|
||||
|
||||
setup(async () => {
|
||||
element = createModelViewerElement(assetPath('models/Astronaut.glb'));
|
||||
composer = new EffectComposer();
|
||||
element.insertBefore(composer, element.firstChild);
|
||||
await waitForEvent(element, 'load');
|
||||
|
||||
baseScreenshot = screenshot(element);
|
||||
colorGrade = new ColorGradeEffect();
|
||||
composer.insertBefore(colorGrade, composer.firstChild);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
test('Color Grade Affects Pixels', async () => {
|
||||
colorGrade.contrast = 1.0;
|
||||
await composer.updateComplete;
|
||||
await rafPasses();
|
||||
const colorGradeScreenshot = screenshot(element);
|
||||
|
||||
expect(ArraysAreEqual(baseScreenshot, colorGradeScreenshot)).to.be.false;
|
||||
expect(CompareArrays(baseScreenshot, colorGradeScreenshot))
|
||||
.to.be.lessThan(0.98);
|
||||
});
|
||||
|
||||
test('Saturation = 0', async () => {
|
||||
colorGrade.saturation = -1;
|
||||
await composer.updateComplete;
|
||||
await rafPasses();
|
||||
const colorGradeScreenshot = screenshot(element);
|
||||
const hslBefore = AverageHSL(baseScreenshot);
|
||||
const hslAfter = AverageHSL(colorGradeScreenshot);
|
||||
expect(hslBefore.s).to.be.greaterThan(hslAfter.s);
|
||||
expect(hslAfter.s).to.be.closeTo(0, 0.01);
|
||||
});
|
||||
|
||||
test('Brightness = 0', async () => {
|
||||
colorGrade.brightness = -1;
|
||||
await composer.updateComplete;
|
||||
await rafPasses();
|
||||
const colorGradeScreenshot = screenshot(element);
|
||||
const hslBefore = AverageHSL(baseScreenshot);
|
||||
const hslAfter = AverageHSL(colorGradeScreenshot);
|
||||
expect(hslBefore.l).to.be.greaterThan(hslAfter.l);
|
||||
expect(hslAfter.l).to.be.eq(0);
|
||||
});
|
||||
|
||||
test('Hue difference', async () => {
|
||||
colorGrade.brightness = colorGrade.contrast = colorGrade.saturation = 0;
|
||||
colorGrade.hue = 2;
|
||||
await composer.updateComplete;
|
||||
await rafPasses();
|
||||
const colorGradeScreenshot = screenshot(element);
|
||||
const hslBefore = AverageHSL(baseScreenshot);
|
||||
const hslAfter = AverageHSL(colorGradeScreenshot);
|
||||
expect(hslBefore.h).to.not.eq(hslAfter.h);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './color-grade-spec.js';
|
||||
@@ -0,0 +1,18 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './utilities-spec.js';
|
||||
import './effect-composer-spec.js';
|
||||
import './effects.js';
|
||||
@@ -0,0 +1,79 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {ModelViewerElement} from '@google/model-viewer';
|
||||
import {Renderer} from '@google/model-viewer/lib/three-components/Renderer.js';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {$effectComposer} from '../effect-composer.js';
|
||||
import {EffectComposer} from '../model-viewer-effects.js';
|
||||
import {getOwnPropertySymbolValue} from '../utilities.js';
|
||||
|
||||
import {ArraysAreEqual, assetPath, createModelViewerElement, screenshot, timePasses, waitForEvent} from './utilities.js';
|
||||
|
||||
suite('Screenshot Baseline Test', () => {
|
||||
let element: ModelViewerElement;
|
||||
let baseScreenshot: Uint8Array;
|
||||
|
||||
setup(async () => {
|
||||
element = createModelViewerElement(assetPath('models/Astronaut.glb'));
|
||||
await waitForEvent(element, 'load');
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
test('Compare ModelViewer to Self', async () => {
|
||||
const renderer =
|
||||
getOwnPropertySymbolValue<Renderer>(element, 'renderer') as Renderer;
|
||||
expect(renderer).to.not.be.undefined;
|
||||
expect(renderer.threeRenderer).to.not.be.undefined;
|
||||
await timePasses(5);
|
||||
baseScreenshot = screenshot(element);
|
||||
await timePasses(5);
|
||||
const screenshot2 = screenshot(element);
|
||||
|
||||
expect(ArraysAreEqual(baseScreenshot, screenshot2)).to.be.true;
|
||||
});
|
||||
|
||||
suite('<effect-composer>', () => {
|
||||
let composer: EffectComposer;
|
||||
let composerScreenshot: Uint8Array;
|
||||
|
||||
setup(async () => {
|
||||
composer = new EffectComposer();
|
||||
composer.renderMode = 'quality';
|
||||
composer.msaa = 8;
|
||||
element.insertBefore(composer, element.firstChild);
|
||||
await timePasses(5);
|
||||
});
|
||||
|
||||
test('Compare Self', async () => {
|
||||
const renderer = composer[$effectComposer].getRenderer();
|
||||
expect(renderer).to.not.be.undefined;
|
||||
await timePasses(10);
|
||||
composerScreenshot = screenshot(element);
|
||||
await timePasses(10);
|
||||
const screenshot2 = screenshot(element);
|
||||
|
||||
expect(ArraysAreEqual(composerScreenshot, screenshot2)).to.be.true;
|
||||
});
|
||||
|
||||
test('Empty EffectComposer and base Renderer are identical', () => {
|
||||
expect(ArraysAreEqual(baseScreenshot, composerScreenshot)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {ModelViewerElement} from '@google/model-viewer';
|
||||
import {Renderer} from '@google/model-viewer/lib/three-components/Renderer.js';
|
||||
import {HSL} from 'three';
|
||||
|
||||
import {getOwnPropertySymbolValue} from '../utilities.js';
|
||||
|
||||
export const timePasses = (ms: number = 0): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* Converts a partial URL string to a fully qualified URL string.
|
||||
*
|
||||
* @param {String} url
|
||||
* @return {String}
|
||||
*/
|
||||
export const toFullUrl = (partialUrl: string): string => {
|
||||
const url = new URL(partialUrl, window.location.toString());
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const deserializeUrl = (url: string|null): string|null =>
|
||||
(!!url && url !== 'null' ? toFullUrl(url) : null);
|
||||
|
||||
export const elementFromLocalPoint =
|
||||
(document: Document|ShadowRoot, x: number, y: number): Element|null => {
|
||||
const host: HTMLElement = document === window.document ?
|
||||
window.document.body :
|
||||
((document as ShadowRoot).host as HTMLElement);
|
||||
const actualDocument =
|
||||
(window as any).ShadyCSS ? window.document : document;
|
||||
const boundingRect = host.getBoundingClientRect();
|
||||
|
||||
return actualDocument.elementFromPoint(
|
||||
boundingRect.left + x, boundingRect.top + y);
|
||||
};
|
||||
|
||||
export const pickShadowDescendant =
|
||||
(element: Element, x: number = 0, y: number = 0): Element|null => {
|
||||
return element.shadowRoot != null ?
|
||||
elementFromLocalPoint(element.shadowRoot, x, y) :
|
||||
null;
|
||||
};
|
||||
|
||||
export const rafPasses = (): Promise<void> =>
|
||||
new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
||||
|
||||
export interface SyntheticEventProperties {
|
||||
clientX?: number;
|
||||
clientY?: number;
|
||||
deltaY?: number;
|
||||
key?: string;
|
||||
shiftKey?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a synthetic event on a given element with a given type, and
|
||||
* optionally with custom event properties. Returns the dispatched event.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {type} string
|
||||
* @param {*} properties
|
||||
*/
|
||||
export const dispatchSyntheticEvent =
|
||||
(target: EventTarget, type: string, properties: SyntheticEventProperties = {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
deltaY: 1.0,
|
||||
}): CustomEvent => {
|
||||
const event = new CustomEvent(type, {cancelable: true, bubbles: true});
|
||||
Object.assign(event, properties);
|
||||
target.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
||||
|
||||
export const ASSETS_DIRECTORY = 'packages/shared-assets/';
|
||||
|
||||
/**
|
||||
* Returns the full path for an asset by name. This is a convenience helper so
|
||||
* that we don't need to change paths throughout all test suites if we ever
|
||||
* decide to move files around.
|
||||
*
|
||||
* @param {string} name
|
||||
* @return {string}
|
||||
*/
|
||||
export const assetPath = (name: string): string =>
|
||||
deserializeUrl(`${ASSETS_DIRECTORY}${name}`)!;
|
||||
|
||||
/**
|
||||
* Returns true if the given element is in the tree of the document of the
|
||||
* current frame.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const isInDocumentTree = (node: Node): boolean => {
|
||||
let root: Node = node.getRootNode();
|
||||
|
||||
while (root !== node && root != null) {
|
||||
if (root.nodeType === Node.DOCUMENT_NODE) {
|
||||
return root === document;
|
||||
}
|
||||
|
||||
root = (root as ShadowRoot).host && (root as ShadowRoot).host.getRootNode();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* "Spies" on a particular object by replacing a specified part of its
|
||||
* implementation with a custom version. Returns a function that restores the
|
||||
* original implementation to the object when invoked.
|
||||
*/
|
||||
export const spy =
|
||||
(object: Object, property: string, descriptor: PropertyDescriptor): (
|
||||
() => void) => {
|
||||
let sourcePrototype = object;
|
||||
|
||||
while (sourcePrototype != null &&
|
||||
!sourcePrototype.hasOwnProperty(property)) {
|
||||
sourcePrototype = (sourcePrototype as any).__proto__;
|
||||
}
|
||||
|
||||
if (sourcePrototype == null) {
|
||||
throw new Error(`Cannot spy property "${property}" on ${object}`);
|
||||
}
|
||||
|
||||
const originalDescriptor =
|
||||
Object.getOwnPropertyDescriptor(sourcePrototype, property);
|
||||
|
||||
if (originalDescriptor == null) {
|
||||
throw new Error(`Cannot read descriptor of "${property}" on ${object}`);
|
||||
}
|
||||
|
||||
Object.defineProperty(sourcePrototype, property, descriptor);
|
||||
|
||||
return () => {
|
||||
Object.defineProperty(sourcePrototype, property, originalDescriptor);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a ModelViewerElement with a given src, attaches to document as first
|
||||
* child and returns
|
||||
* @param src Model to load
|
||||
* @returns element
|
||||
*/
|
||||
export const createModelViewerElement =
|
||||
(src: string|null): ModelViewerElement => {
|
||||
const element = new ModelViewerElement();
|
||||
document.body.insertBefore(element, document.body.firstChild);
|
||||
element.src = src;
|
||||
return element;
|
||||
};
|
||||
|
||||
export type PredicateFunction<T = void> = (value: T) => boolean;
|
||||
|
||||
/**
|
||||
* @param {EventTarget|EventDispatcher} target
|
||||
* @param {string} eventName
|
||||
* @param {?Function} predicate
|
||||
*/
|
||||
export const waitForEvent =
|
||||
<T>(target: any,
|
||||
eventName: string,
|
||||
predicate: PredicateFunction<T>|null = null): Promise<T> =>
|
||||
new Promise((resolve) => {
|
||||
function handler(event: T) {
|
||||
if (!predicate || predicate(event)) {
|
||||
resolve(event);
|
||||
target.removeEventListener(eventName, handler);
|
||||
}
|
||||
}
|
||||
target.addEventListener(eventName, handler);
|
||||
});
|
||||
|
||||
export interface TypedArray<T = unknown> {
|
||||
readonly BYTES_PER_ELEMENT: number;
|
||||
length: number;
|
||||
[n: number]: T;
|
||||
reduce(
|
||||
callbackfn:
|
||||
(previousValue: number, currentValue: number, currentIndex: number,
|
||||
array: TypedArray<number>) => number,
|
||||
initialValue?: number): number;
|
||||
}
|
||||
|
||||
const COMPONENTS_PER_PIXEL = 4;
|
||||
|
||||
export function screenshot(element: ModelViewerElement): Uint8Array {
|
||||
const renderer = getOwnPropertySymbolValue<Renderer>(element, 'renderer');
|
||||
if (!renderer)
|
||||
throw new Error('Invalid element provided');
|
||||
|
||||
const screenshotContext = renderer.threeRenderer.getContext();
|
||||
const width = screenshotContext.drawingBufferWidth;
|
||||
const height = screenshotContext.drawingBufferHeight;
|
||||
|
||||
const pixels = new Uint8Array(width * height * COMPONENTS_PER_PIXEL);
|
||||
// this function reads in the bottom-up direction from the coordinate
|
||||
// specified ((0,0) is the bottom-left corner).
|
||||
screenshotContext.readPixels(
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
screenshotContext.RGBA,
|
||||
screenshotContext.UNSIGNED_BYTE,
|
||||
pixels);
|
||||
|
||||
return pixels;
|
||||
}
|
||||
|
||||
export function ArraysAreEqual(arr1: TypedArray, arr2: TypedArray): boolean {
|
||||
if (arr1.length !== arr2.length)
|
||||
return false;
|
||||
|
||||
for (let i = 0; i < arr1.length; i++) {
|
||||
if (arr1[i] !== arr2[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compares two
|
||||
* @param arr1
|
||||
* @param arr2
|
||||
* @returns Percentage of similarity (0-1), higher is better
|
||||
*/
|
||||
export function CompareArrays(
|
||||
arr1: TypedArray<number>, arr2: TypedArray<number>): number {
|
||||
if (arr1.length !== arr2.length ||
|
||||
arr1.BYTES_PER_ELEMENT !== arr2.BYTES_PER_ELEMENT)
|
||||
return 0;
|
||||
|
||||
const similarity: number[] = [];
|
||||
const max = maxValue(arr1.BYTES_PER_ELEMENT);
|
||||
for (let i = 0; i < arr1.length; i += COMPONENTS_PER_PIXEL) {
|
||||
if (arr1[i + 3] != 0 && arr2[i + 3] != 0) { // a
|
||||
similarity.push(1 - percentage(arr1[i], arr2[i], max)); // r
|
||||
similarity.push(1 - percentage(arr1[i + 1], arr2[i + 1], max)); // g
|
||||
similarity.push(1 - percentage(arr1[i + 2], arr2[i + 2], max)); // b
|
||||
}
|
||||
}
|
||||
return average(similarity);
|
||||
}
|
||||
|
||||
export function AverageHSL(arr: TypedArray<number>): HSL {
|
||||
const H: number[] = [];
|
||||
const S: number[] = [];
|
||||
const L: number[] = [];
|
||||
for (let i = 0; i < arr.length; i += COMPONENTS_PER_PIXEL) {
|
||||
if (arr[i + 3] != 0) {
|
||||
// a
|
||||
const hsl = rgbToHsl(arr[i], arr[i + 1], arr[i + 2]);
|
||||
H.push(hsl.h);
|
||||
S.push(hsl.s);
|
||||
L.push(hsl.l);
|
||||
}
|
||||
}
|
||||
return {h: average(H), s: average(S), l: average(L)};
|
||||
}
|
||||
|
||||
function maxValue(bytes: number): number {
|
||||
return Math.pow(2, 8 * bytes) - 1;
|
||||
}
|
||||
|
||||
function percentage(n1: number, n2: number, maxN: number): number {
|
||||
return Math.abs(n1 - n2) / maxN;
|
||||
}
|
||||
|
||||
function average(arr: number[]): number {
|
||||
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RGB color value to HSL. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes r, g, and b are contained in the set [0, 255] and
|
||||
* returns h, s, and l in the set [0, 1].
|
||||
*
|
||||
* @param Number r The red color value
|
||||
* @param Number g The green color value
|
||||
* @param Number b The blue color value
|
||||
* @return Array The HSL representation
|
||||
*/
|
||||
function rgbToHsl(r: number, g: number, b: number): HSL {
|
||||
(r /= 255), (g /= 255), (b /= 255);
|
||||
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if (max == min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
var d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
default:
|
||||
throw new Error('invalid rgb');
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return {h, s, l};
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/* @license
|
||||
* Copyright 2023 Google LLC. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Effect, EffectAttribute, EffectPass, Pass } from 'postprocessing';
|
||||
import { ColorRepresentation } from 'three';
|
||||
|
||||
export type Constructor<T = object, U = object> = {
|
||||
new (...args: any[]): T;
|
||||
prototype: T;
|
||||
} & U;
|
||||
|
||||
/**
|
||||
* Get symbol of given key if exists on object.
|
||||
* @param object Object to retrieve symbol from
|
||||
* @param key Key to search for (case sensitive)
|
||||
* @returns `Symbol(key)`
|
||||
*/
|
||||
export function getOwnPropertySymbol(object: any, key: string): symbol | undefined {
|
||||
while (object) {
|
||||
const symbol = Object.getOwnPropertySymbols(object).find((symbol) => symbol.description === key);
|
||||
if (symbol) return symbol;
|
||||
// Search further up in prototype chain
|
||||
object = Object.getPrototypeOf(object);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether an object has a Symbol property with the specified key.
|
||||
* @param object Object to retrieve symbol from
|
||||
* @param key Key to search for (case sensitive)
|
||||
*/
|
||||
export function hasOwnPropertySymbol(object: any, key: string): boolean {
|
||||
return getOwnPropertySymbol(object, key) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value of symbol of given key if exists on object.
|
||||
* @param object Object to retrieve key value from
|
||||
* @param key Key to search for (case sensitive)
|
||||
* @returns `object[Symbol(key)]`
|
||||
*/
|
||||
export function getOwnPropertySymbolValue<T = unknown>(object: any, key: string): T | undefined {
|
||||
const symbol = getOwnPropertySymbol(object, key);
|
||||
return symbol && object[symbol];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} value
|
||||
* @param {Number} lowerLimit
|
||||
* @param {Number} upperLimit
|
||||
* @return {Number} value clamped within `lowerLimit - upperLimit`
|
||||
*/
|
||||
export function clamp(value: number, lowerLimit: number, upperLimit: number): number {
|
||||
return Math.max(lowerLimit, Math.min(upperLimit, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} value
|
||||
* @returns value clamped between `0 - 1`
|
||||
*/
|
||||
export function clampNormal(value: number): number {
|
||||
return clamp(value, 0, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} value
|
||||
* @param {Number} lowerLimit
|
||||
* @param {Number} upperLimit
|
||||
* @return {Number} wraps value between `lowerLimit - upperLimit`
|
||||
*/
|
||||
export function wrapClamp(value: number, lowerLimit: number, upperLimit: number): number {
|
||||
if (value > upperLimit) return lowerLimit;
|
||||
if (value < lowerLimit) return upperLimit;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches through hierarchy of HTMLElement until an element with a non-transparent background is found
|
||||
* @param elem The element background to get
|
||||
* @returns The backgroundColor
|
||||
*/
|
||||
export function getBackgroundColor(elem: HTMLElement): ColorRepresentation | undefined {
|
||||
let currElem: HTMLElement | null = elem;
|
||||
while (currElem && isTransparent(getComputedStyle(currElem))) {
|
||||
currElem = currElem.parentElement;
|
||||
}
|
||||
if (!currElem) return;
|
||||
return getComputedStyle(currElem).backgroundColor as ColorRepresentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether an Element's backgroundColor is transparent
|
||||
* @param style The CSS properties of an Element
|
||||
*/
|
||||
function isTransparent(style: CSSStyleDeclaration): boolean {
|
||||
return style.backgroundColor === 'transparent' || style.backgroundColor === 'rgba(0, 0, 0, 0)' || !style.backgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given Effect uses Convolution.
|
||||
* @param effect The effect to check.
|
||||
*/
|
||||
export function isConvolution(effect: Effect): boolean {
|
||||
return (effect.getAttributes() & EffectAttribute.CONVOLUTION) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of Pass properties without disposing of the Effects.
|
||||
* @param pass Pass to dispose of
|
||||
*/
|
||||
export function disposeEffectPass(pass: EffectPass): void {
|
||||
Pass.prototype.dispose.call(pass);
|
||||
|
||||
if (!(pass as any).listener) return;
|
||||
for (const effect of (pass as any).effects) {
|
||||
effect.removeEventListener('change', (pass as any).listener);
|
||||
}
|
||||
}
|
||||
|
||||
export function getValueOfEnum<T extends Object>(Enum: T, key: string): T {
|
||||
const index = Object.keys(Enum)
|
||||
.filter((v) => !isNaN(Number(v)))
|
||||
.indexOf(key);
|
||||
return (Enum as any)[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to validate whether a value is in-fact a valid option of a literal type.
|
||||
*
|
||||
* Requires the type to be defined as follows:
|
||||
* @code
|
||||
* `const TOptions = [...] as const;`
|
||||
*
|
||||
* `type T = typeof TOptions[number];`
|
||||
* @param options `TOptions`
|
||||
* @param value `value: T`
|
||||
* @throws TypeError
|
||||
*/
|
||||
export function validateLiteralType<TOptions extends readonly unknown[]>(options: TOptions, value: typeof options[number]): void {
|
||||
if (!options.includes(value)) throw new TypeError(`Validation Error: ${value} is not a valid value. Expected ${options.join(' | ')}`);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"outDir": "./lib",
|
||||
"experimentalDecorators": true,
|
||||
"noErrorTruncation": true,
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// import {esbuildPlugin} from '@web/dev-server-esbuild';
|
||||
import {devices, playwrightLauncher} from '@web/test-runner-playwright';
|
||||
|
||||
export default {
|
||||
concurrency: 10,
|
||||
nodeResolve: true,
|
||||
files: 'lib/test/**/*-spec.js',
|
||||
// in a monorepo you need to set set the root dir to resolve modules
|
||||
rootDir: '../../',
|
||||
browserLogs: false,
|
||||
filterBrowserLogs:
|
||||
(log) => {
|
||||
return log.type === 'error';
|
||||
},
|
||||
testRunnerHtml: testFramework => `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="${testFramework}"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
testsFinishTimeout: 300000,
|
||||
testFramework: {
|
||||
config: {
|
||||
ui: 'tdd',
|
||||
timeout: '120000',
|
||||
},
|
||||
},
|
||||
// plugins: [esbuildPlugin({ts: true})],
|
||||
// browsers:
|
||||
// [
|
||||
// playwrightLauncher({
|
||||
// product: 'webkit',
|
||||
// createBrowserContext({browser}) {
|
||||
// return browser.newContext({...devices['iPhone X']});
|
||||
// },
|
||||
// }),
|
||||
// ],
|
||||
};
|
||||
Reference in new issue
Block a user