diff --git a/CHANGELOG.md b/CHANGELOG.md index ce41498..3e84a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,20 @@ The format is based on [Keep a Changelog](https://github.com/olivierlacan/keep-a ### Removed +## [2.0.0] - 2023-12-16 + +### Added + +- Add camera animation for viewer with `animejs`. (https://github.com/yushiang-demo/pano-to-mesh/pull/68) + +### Changed + +- Change README.md demo links and snapshots. (https://github.com/yushiang-demo/pano-to-mesh/pull/50) + +### Fixed + +### Removed + ## [1.4.1] - 2023-11-14 ### Added @@ -171,7 +185,8 @@ Codes without pull requests won't be recorded. ### Removed -[unreleased]: https://github.com/yushiang-demo/PanoToMesh/compare/v1.4.1...HEAD +[unreleased]: https://github.com/yushiang-demo/PanoToMesh/compare/v2.0.0...HEAD +[2.0.0]: https://github.com/yushiang-demo/PanoToMesh/compare/v1.4.1...v2.0.0 [1.4.1]: https://github.com/yushiang-demo/PanoToMesh/compare/v1.4.0...v1.4.1 [1.4.0]: https://github.com/yushiang-demo/PanoToMesh/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/yushiang-demo/PanoToMesh/compare/v1.2.0...v1.3.0 diff --git a/Demo.png b/Demo.png deleted file mode 100644 index 9692967..0000000 Binary files a/Demo.png and /dev/null differ diff --git a/README.md b/README.md index 575a22f..8ca45d0 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,50 @@ -# Quick start +# Pano-to-mesh -## Development with Yarn +Our tool simplifies 3D panorama creation. You can annotate layouts, preview 3D meshes, export them with textures, arrange your media, share a viewer link for collaboration, and also experience immersive interaction through a first-person view. + +## Quick start + +### Development ``` yarn yarn dev ``` -## Production with Docker +### Production -### From Dockerfile +- From Dockerfile ``` docker build . -t pano-to-mesh --no-cache docker run -p 3000:3000 pano-to-mesh ``` -### From Github Docker Registry +- From Github Docker Registry ``` docker run -p 3000:3000 ghcr.io/yushiang-demo/pano-to-mesh ``` -# Introduction - -## Demo - -### Editor - -- [Panorama to mesh](https://pano-to-mesh.vercel.app/editors/layout2d#eNpdkE9rwzAMxb-Lz8aWHP-Rcyyjt7WHwqCUENKsSTMyJ6TutjL23ecUtrDp8EPiPZ6EPlnTD8O0ZzlwVp-6vgttGlgmtGac9dVtuEb1wPLDAQTav5VxEIZcBriw4MmoQQM6Mg61mmlmo7fGAukf3o1Wg1WzmhqFuPCueufsvxQLhhTgL4uCs7EKw1S9Vunsc4zjJZeyuqBoYv0cRDhF-abky9hKQEleAklHEgGgXJdIHsgRuXK7mvqsQXT7rh693q3O6_fH28dTe9yYTVMfRUpgy67t1LVdSG8BjmJ-RPH1DUTpVnM) -- [Mesh preview and download](https://pano-to-mesh.vercel.app/editors/layout3d#eNpdkE9rwzAMxb-Lz8aWHP-Rcyyjt7WHwqCUENKsSTMyJ6TutjL23ecUtrDp8EPiPZ6EPlnTD8O0ZzlwVp-6vgttGlgmtGac9dVtuEb1wPLDAQTav5VxEIZcBriw4MmoQQM6Mg61mmlmo7fGAukf3o1Wg1WzmhqFuPCueufsvxQLhhTgL4uCs7EKw1S9Vunsc4zjJZeyuqBoYv0cRDhF-abky9hKQEleAklHEgGgXJdIHsgRuXK7mvqsQXT7rh693q3O6_fH28dTe9yYTVMfRUpgy67t1LVdSG8BjmJ-RPH1DUTpVnM) -- [Decoration with media](https://pano-to-mesh.vercel.app/editors/decoration#eNpdkE9rwzAMxb-Lz8aWHP-Rcyyjt7WHwqCUENKsSTMyJ6TutjL23ecUtrDp8EPiPZ6EPlnTD8O0ZzlwVp-6vgttGlgmtGac9dVtuEb1wPLDAQTav5VxEIZcBriw4MmoQQM6Mg61mmlmo7fGAukf3o1Wg1WzmhqFuPCueufsvxQLhhTgL4uCs7EKw1S9Vunsc4zjJZeyuqBoYv0cRDhF-abky9hKQEleAklHEgGgXJdIHsgRuXK7mvqsQXT7rh693q3O6_fH28dTe9yYTVMfRUpgy67t1LVdSG8BjmJ-RPH1DUTpVnM) +## Features -![image](./Demo.png) +- Annotate panorama layouts and preview 3D meshes. +- Export meshes and textures. +- Arrange media, including 3D meshes and HTML elements. +- Experience a first-person view and interact in the viewer. -### Viewer +| [Panorama to mesh] | [Mesh preview and download] | +| :----------------------: | :-------------------------: | +| ![2d.png](./docs/2d.png) | ![3d.png](./docs/3d.png) | -- [Viewer](https://pano-to-mesh.vercel.app#eNpdkE9rwzAMxb-Lz8aWHP-Rcyyjt7WHwqCUENKsSTMyJ6TutjL23ecUtrDp8EPiPZ6EPlnTD8O0ZzlwVp-6vgttGlgmtGac9dVtuEb1wPLDAQTav5VxEIZcBriw4MmoQQM6Mg61mmlmo7fGAukf3o1Wg1WzmhqFuPCueufsvxQLhhTgL4uCs7EKw1S9Vunsc4zjJZeyuqBoYv0cRDhF-abky9hKQEleAklHEgGgXJdIHsgRuXK7mvqsQXT7rh693q3O6_fH28dTe9yYTVMfRUpgy67t1LVdSG8BjmJ-RPH1DUTpVnM) +| [Decoration with media] | [Viewer] | +| :----------------------------: | :-----------------------------: | +| ![media.png](./docs/media.png) | ![view.png](./docs//viewer.png) | > Demo image is provided by: > https://as1.ftcdn.net/v2/jpg/01/89/08/78/1000_F_189087887_OBrl3f117Yicp94SBhFwMyxVgbN5Nfcb.jpg -## Features - -- Load panorama from url. -- Annotate layout from a panorama and preview 3D Mesh. -- Save 3D mesh and texture to local. -- Share result of viewer and editor with data embedded in URL. -- Editor of arranging media for viewer. +[Panorama to mesh]: https://pano-to-mesh.vercel.app/editors/layout2d#eNrNl19vGzcQxL9Kcc-XM_8suaSf89KHokVSFC1co7jYZ1mtpFMlGagR-Lv3t1ITSJGdKIkLRBBki-LdcYczO8O3zbJfjKt-3jfnzdlqWI93q6thffZutPtzOWna95N-XE0n00VzfuFa36XWXbbNzWwcV781565trobpbLqY8KWJXYxcN-vvx7tNeMkVF67LMfoiKcTiqn22NuRC9c6l3We8bJkXa06q72a5GpmYqob913aiz6FKllCSej7zdmLRyvD793Zi1SIhZF-dKJ_2YJel-PfvdEkp8-F6Cg4Xb5vNql-sb8bVvN9MR-p92yzH9XT3_8UL37lSJGsNKhrE81zfFe9zjb5ahTGqtKFLOWjiq69KmUV4xPqqnw3cI3bOB599VldEVEtkvovBxSjM9S5w__Z4Frf4-67fDKvFbi2OUooLPCnzp7joom_dFq-csjKQxIG7XD60zeZ-ybObX6bXw8jmrIb-elzM7pvzm362Htrmut_0Vut6dXXIhi0w3XwpzcND-yl0qENYcpJaYmRZBXAkaSilGjo1ssb2BTvAN6pLxUnVHMsBOjGoyz55cdV5DTtwqnPFx1qSz7k9mvMc0Ly-7a-H1XdfgxAyGGfjqjln11Sp79OQUYpLmjOrrDH5IEq9vgSrqgT4rD4BmQELg6nZJ0215kM-MQAdIaYmJJNAHT4ml0SQhkguvj2edQzadmdAOEUH7QIbFraocQ3Q80sqNedU9lD7ft5Phi-Aa7mYnEAo6RxKK67kADIoxFpPVHQMvFFThGoMOYlVAjpJVdjgcAgOmpRATUg27aCBPIhTUWzNYnQ6nPMswPxHp6_B5wvoZLT3KpVqTIBarOBEj0SQjKM1FegUO2st1rZiSpn-eYCYZtopjbjkLHAow8iybZlwr9Lg6N4RWQNWTakgSAilj0nQR5_o47TxYMJLW8xq9cgxhZxrcjGHfTLdYDQnoHW72SzX52dnm_WwmNzfrW-n_WLSTaab27s33XQ8W4wbgDylYWE4ScU5oekG6kMooQsZr8p0L6mMBlNfxapctvaOVHM6gCvRwLxxoJZcIQYMg2ASksPBog-xGkVj0Wx3K-qpuzwHXD8P_2w-BtbGfj9vXg7z8ffFq3Gcn4IIbuZEos-IyUWhFVlDqhRWIjjhtIyw1iJmawVZ0r2C30PE2bqfoyP_NOuvhttxZjoiRzxd6UllVfHBGmhE--rj1qPFFeyVFMGgU9toUSlAr1GwX9RySl2fu2_PW1dBhSZVnkfgcBY-tFJqwCxrjUFYEIXhBoZxNhQ8qv72C9vuFDGuKFmLnEBhtZCQotHFw9KtMjG6gkk6lUxrTulbr8t1YiEmZUyLeFh2jq-0eCKrVxKS7loOQouKYSdhUb5-43WhL-eNdmJbVWiS1gfxZ6qg7zkXyTPYRsLIqdr6aMBK9qoSnD1hUuR04jqR0WAJ2QxXwYWWmtqjOY9adS2cDOjFmUhVS9lZtSdEizpvtshy_P_UaIKddCqHFJIIIcULj0Zv-y_xtuMQFlrTETFhQat7SHAPnCJTBquF7SKWeGEI8ZCaSG-ixyNHULASOh2wKZ5D3PGGjaVN5aQBRcgEduunBj-8eg-vH4b17WdlmtdgOx1W3WT2xlLNbLrk91d3i-aURCPwF2aH7YHQU4YhauRgvZhRxmA5PtZ4gKBgt4kYnZNdvUWc7BaZD0eLxbb2eNKHEIIC51IsAyAKO0X4cbahoAInYZJy0nGaHx86vvRrIPz1zbg5xG-9GPq__gAqS0yb6dxua3KvzSlyxSCUUwcLTLZWDgd7L-9qSweGwqTaoGonD866ewjTntV7zmRwkBRA-kXv5EMykp31yEIkqvZ41iMQE5oQOZkoVYuoamJlq4LpnIxanBb_xNiHlz6h6fhxTV8-_Avsc5U2 +[Mesh preview and download]: https://pano-to-mesh.vercel.app/editors/layout3d#eNrNl19vGzcQxL9Kcc-XM_8suaSf89KHokVSFC1co7jYZ1mtpFMlGagR-Lv3t1ITSJGdKIkLRBBki-LdcYczO8O3zbJfjKt-3jfnzdlqWI93q6thffZutPtzOWna95N-XE0n00VzfuFa36XWXbbNzWwcV781565trobpbLqY8KWJXYxcN-vvx7tNeMkVF67LMfoiKcTiqn22NuRC9c6l3We8bJkXa06q72a5GpmYqob913aiz6FKllCSej7zdmLRyvD793Zi1SIhZF-dKJ_2YJel-PfvdEkp8-F6Cg4Xb5vNql-sb8bVvN9MR-p92yzH9XT3_8UL37lSJGsNKhrE81zfFe9zjb5ahTGqtKFLOWjiq69KmUV4xPqqnw3cI3bOB599VldEVEtkvovBxSjM9S5w__Z4Frf4-67fDKvFbi2OUooLPCnzp7joom_dFq-csjKQxIG7XD60zeZ-ybObX6bXw8jmrIb-elzM7pvzm362Htrmut_0Vut6dXXIhi0w3XwpzcND-yl0qENYcpJaYmRZBXAkaSilGjo1ssb2BTvAN6pLxUnVHMsBOjGoyz55cdV5DTtwqnPFx1qSz7k9mvMc0Ly-7a-H1XdfgxAyGGfjqjln11Sp79OQUYpLmjOrrDH5IEq9vgSrqgT4rD4BmQELg6nZJ0215kM-MQAdIaYmJJNAHT4ml0SQhkguvj2edQzadmdAOEUH7QIbFraocQ3Q80sqNedU9lD7ft5Phi-Aa7mYnEAo6RxKK67kADIoxFpPVHQMvFFThGoMOYlVAjpJVdjgcAgOmpRATUg27aCBPIhTUWzNYnQ6nPMswPxHp6_B5wvoZLT3KpVqTIBarOBEj0SQjKM1FegUO2st1rZiSpn-eYCYZtopjbjkLHAow8iybZlwr9Lg6N4RWQNWTakgSAilj0nQR5_o47TxYMJLW8xq9cgxhZxrcjGHfTLdYDQnoHW72SzX52dnm_WwmNzfrW-n_WLSTaab27s33XQ8W4wbgDylYWE4ScU5oekG6kMooQsZr8p0L6mMBlNfxapctvaOVHM6gCvRwLxxoJZcIQYMg2ASksPBog-xGkVj0Wx3K-qpuzwHXD8P_2w-BtbGfj9vXg7z8ffFq3Gcn4IIbuZEos-IyUWhFVlDqhRWIjjhtIyw1iJmawVZ0r2C30PE2bqfoyP_NOuvhttxZjoiRzxd6UllVfHBGmhE--rj1qPFFeyVFMGgU9toUSlAr1GwX9RySl2fu2_PW1dBhSZVnkfgcBY-tFJqwCxrjUFYEIXhBoZxNhQ8qv72C9vuFDGuKFmLnEBhtZCQotHFw9KtMjG6gkk6lUxrTulbr8t1YiEmZUyLeFh2jq-0eCKrVxKS7loOQouKYSdhUb5-43WhL-eNdmJbVWiS1gfxZ6qg7zkXyTPYRsLIqdr6aMBK9qoSnD1hUuR04jqR0WAJ2QxXwYWWmtqjOY9adS2cDOjFmUhVS9lZtSdEizpvtshy_P_UaIKddCqHFJIIIcULj0Zv-y_xtuMQFlrTETFhQat7SHAPnCJTBquF7SKWeGEI8ZCaSG-ixyNHULASOh2wKZ5D3PGGjaVN5aQBRcgEduunBj-8eg-vH4b17WdlmtdgOx1W3WT2xlLNbLrk91d3i-aURCPwF2aH7YHQU4YhauRgvZhRxmA5PtZ4gKBgt4kYnZNdvUWc7BaZD0eLxbb2eNKHEIIC51IsAyAKO0X4cbahoAInYZJy0nGaHx86vvRrIPz1zbg5xG-9GPq__gAqS0yb6dxua3KvzSlyxSCUUwcLTLZWDgd7L-9qSweGwqTaoGonD866ewjTntV7zmRwkBRA-kXv5EMykp31yEIkqvZ41iMQE5oQOZkoVYuoamJlq4LpnIxanBb_xNiHlz6h6fhxTV8-_Avsc5U2 +[Decoration with media]: https://pano-to-mesh.vercel.app/editors/decoration#eNrNl19vGzcQxL9Kcc-XM_8suaSf89KHokVSFC1co7jYZ1mtpFMlGagR-Lv3t1ITSJGdKIkLRBBki-LdcYczO8O3zbJfjKt-3jfnzdlqWI93q6thffZutPtzOWna95N-XE0n00VzfuFa36XWXbbNzWwcV781565trobpbLqY8KWJXYxcN-vvx7tNeMkVF67LMfoiKcTiqn22NuRC9c6l3We8bJkXa06q72a5GpmYqob913aiz6FKllCSej7zdmLRyvD793Zi1SIhZF-dKJ_2YJel-PfvdEkp8-F6Cg4Xb5vNql-sb8bVvN9MR-p92yzH9XT3_8UL37lSJGsNKhrE81zfFe9zjb5ahTGqtKFLOWjiq69KmUV4xPqqnw3cI3bOB599VldEVEtkvovBxSjM9S5w__Z4Frf4-67fDKvFbi2OUooLPCnzp7joom_dFq-csjKQxIG7XD60zeZ-ybObX6bXw8jmrIb-elzM7pvzm362Htrmut_0Vut6dXXIhi0w3XwpzcND-yl0qENYcpJaYmRZBXAkaSilGjo1ssb2BTvAN6pLxUnVHMsBOjGoyz55cdV5DTtwqnPFx1qSz7k9mvMc0Ly-7a-H1XdfgxAyGGfjqjln11Sp79OQUYpLmjOrrDH5IEq9vgSrqgT4rD4BmQELg6nZJ0215kM-MQAdIaYmJJNAHT4ml0SQhkguvj2edQzadmdAOEUH7QIbFraocQ3Q80sqNedU9lD7ft5Phi-Aa7mYnEAo6RxKK67kADIoxFpPVHQMvFFThGoMOYlVAjpJVdjgcAgOmpRATUg27aCBPIhTUWzNYnQ6nPMswPxHp6_B5wvoZLT3KpVqTIBarOBEj0SQjKM1FegUO2st1rZiSpn-eYCYZtopjbjkLHAow8iybZlwr9Lg6N4RWQNWTakgSAilj0nQR5_o47TxYMJLW8xq9cgxhZxrcjGHfTLdYDQnoHW72SzX52dnm_WwmNzfrW-n_WLSTaab27s33XQ8W4wbgDylYWE4ScU5oekG6kMooQsZr8p0L6mMBlNfxapctvaOVHM6gCvRwLxxoJZcIQYMg2ASksPBog-xGkVj0Wx3K-qpuzwHXD8P_2w-BtbGfj9vXg7z8ffFq3Gcn4IIbuZEos-IyUWhFVlDqhRWIjjhtIyw1iJmawVZ0r2C30PE2bqfoyP_NOuvhttxZjoiRzxd6UllVfHBGmhE--rj1qPFFeyVFMGgU9toUSlAr1GwX9RySl2fu2_PW1dBhSZVnkfgcBY-tFJqwCxrjUFYEIXhBoZxNhQ8qv72C9vuFDGuKFmLnEBhtZCQotHFw9KtMjG6gkk6lUxrTulbr8t1YiEmZUyLeFh2jq-0eCKrVxKS7loOQouKYSdhUb5-43WhL-eNdmJbVWiS1gfxZ6qg7zkXyTPYRsLIqdr6aMBK9qoSnD1hUuR04jqR0WAJ2QxXwYWWmtqjOY9adS2cDOjFmUhVS9lZtSdEizpvtshy_P_UaIKddCqHFJIIIcULj0Zv-y_xtuMQFlrTETFhQat7SHAPnCJTBquF7SKWeGEI8ZCaSG-ixyNHULASOh2wKZ5D3PGGjaVN5aQBRcgEduunBj-8eg-vH4b17WdlmtdgOx1W3WT2xlLNbLrk91d3i-aURCPwF2aH7YHQU4YhauRgvZhRxmA5PtZ4gKBgt4kYnZNdvUWc7BaZD0eLxbb2eNKHEIIC51IsAyAKO0X4cbahoAInYZJy0nGaHx86vvRrIPz1zbg5xG-9GPq__gAqS0yb6dxua3KvzSlyxSCUUwcLTLZWDgd7L-9qSweGwqTaoGonD866ewjTntV7zmRwkBRA-kXv5EMykp31yEIkqvZ41iMQE5oQOZkoVYuoamJlq4LpnIxanBb_xNiHlz6h6fhxTV8-_Avsc5U2 +[Viewer]: https://pano-to-mesh.vercel.app/#eNrNl19vGzcQxL9Kcc-XM_8suaSf89KHokVSFC1co7jYZ1mtpFMlGagR-Lv3t1ITSJGdKIkLRBBki-LdcYczO8O3zbJfjKt-3jfnzdlqWI93q6thffZutPtzOWna95N-XE0n00VzfuFa36XWXbbNzWwcV781565trobpbLqY8KWJXYxcN-vvx7tNeMkVF67LMfoiKcTiqn22NuRC9c6l3We8bJkXa06q72a5GpmYqob913aiz6FKllCSej7zdmLRyvD793Zi1SIhZF-dKJ_2YJel-PfvdEkp8-F6Cg4Xb5vNql-sb8bVvN9MR-p92yzH9XT3_8UL37lSJGsNKhrE81zfFe9zjb5ahTGqtKFLOWjiq69KmUV4xPqqnw3cI3bOB599VldEVEtkvovBxSjM9S5w__Z4Frf4-67fDKvFbi2OUooLPCnzp7joom_dFq-csjKQxIG7XD60zeZ-ybObX6bXw8jmrIb-elzM7pvzm362Htrmut_0Vut6dXXIhi0w3XwpzcND-yl0qENYcpJaYmRZBXAkaSilGjo1ssb2BTvAN6pLxUnVHMsBOjGoyz55cdV5DTtwqnPFx1qSz7k9mvMc0Ly-7a-H1XdfgxAyGGfjqjln11Sp79OQUYpLmjOrrDH5IEq9vgSrqgT4rD4BmQELg6nZJ0215kM-MQAdIaYmJJNAHT4ml0SQhkguvj2edQzadmdAOEUH7QIbFraocQ3Q80sqNedU9lD7ft5Phi-Aa7mYnEAo6RxKK67kADIoxFpPVHQMvFFThGoMOYlVAjpJVdjgcAgOmpRATUg27aCBPIhTUWzNYnQ6nPMswPxHp6_B5wvoZLT3KpVqTIBarOBEj0SQjKM1FegUO2st1rZiSpn-eYCYZtopjbjkLHAow8iybZlwr9Lg6N4RWQNWTakgSAilj0nQR5_o47TxYMJLW8xq9cgxhZxrcjGHfTLdYDQnoHW72SzX52dnm_WwmNzfrW-n_WLSTaab27s33XQ8W4wbgDylYWE4ScU5oekG6kMooQsZr8p0L6mMBlNfxapctvaOVHM6gCvRwLxxoJZcIQYMg2ASksPBog-xGkVj0Wx3K-qpuzwHXD8P_2w-BtbGfj9vXg7z8ffFq3Gcn4IIbuZEos-IyUWhFVlDqhRWIjjhtIyw1iJmawVZ0r2C30PE2bqfoyP_NOuvhttxZjoiRzxd6UllVfHBGmhE--rj1qPFFeyVFMGgU9toUSlAr1GwX9RySl2fu2_PW1dBhSZVnkfgcBY-tFJqwCxrjUFYEIXhBoZxNhQ8qv72C9vuFDGuKFmLnEBhtZCQotHFw9KtMjG6gkk6lUxrTulbr8t1YiEmZUyLeFh2jq-0eCKrVxKS7loOQouKYSdhUb5-43WhL-eNdmJbVWiS1gfxZ6qg7zkXyTPYRsLIqdr6aMBK9qoSnD1hUuR04jqR0WAJ2QxXwYWWmtqjOY9adS2cDOjFmUhVS9lZtSdEizpvtshy_P_UaIKddCqHFJIIIcULj0Zv-y_xtuMQFlrTETFhQat7SHAPnCJTBquF7SKWeGEI8ZCaSG-ixyNHULASOh2wKZ5D3PGGjaVN5aQBRcgEduunBj-8eg-vH4b17WdlmtdgOx1W3WT2xlLNbLrk91d3i-aURCPwF2aH7YHQU4YhauRgvZhRxmA5PtZ4gKBgt4kYnZNdvUWc7BaZD0eLxbb2eNKHEIIC51IsAyAKO0X4cbahoAInYZJy0nGaHx86vvRrIPz1zbg5xG-9GPq__gAqS0yb6dxua3KvzSlyxSCUUwcLTLZWDgd7L-9qSweGwqTaoGonD866ewjTntV7zmRwkBRA-kXv5EMykp31yEIkqvZ41iMQE5oQOZkoVYuoamJlq4LpnIxanBb_xNiHlz6h6fhxTV8-_Avsc5U2 diff --git a/apps/viewer.js b/apps/viewer.js index 8bf2847..7cad10f 100644 --- a/apps/viewer.js +++ b/apps/viewer.js @@ -1,6 +1,7 @@ -import React, { useRef, useMemo } from "react"; - +import React, { useRef, useMemo, useState, useCallback } from "react"; +import Animator from "@pano-to-mesh/anime"; import { + Core, Loaders, ThreeCanvas, PanoramaProjectionMesh, @@ -10,11 +11,17 @@ import { import useClick2AddWalls from "../hooks/useClick2AddWalls"; import MediaManager from "../components/MediaManager"; import { MEDIA_2D, MEDIA_3D } from "../components/MediaManager/types"; +import useMouseSkipDrag from "../hooks/useMouseSkipDrag"; +import ToolbarRnd from "../components/ToolbarRnd"; +import Toolbar from "../components/Toolbar"; +import Icons from "../components/Icon"; const dev = process.env.NODE_ENV === "development"; const Viewer = ({ data }) => { const threeRef = useRef(null); - + const [isTopView, setIsTopView] = useState(true); + const [isCameraMoving, setIsCameraMoving] = useState(false); + const [baseMesh, setBaseMesh] = useState(null); const geometryInfo = useMemo( () => ({ floorY: data.floorY, @@ -35,22 +42,97 @@ const Viewer = ({ data }) => { panorama: Loaders.useTexture({ src: data.panorama }), }; - const onLoad = (mesh) => { + const onLoad = useCallback((mesh) => { threeRef.current.cameraControls.focus(mesh); - }; + setBaseMesh(mesh); + }, []); const media = (data.media || []).filter( (data) => ![MEDIA_3D.PLACEHOLDER_3D, MEDIA_2D.PLACEHOLDER_2D].includes(data.type) ); + const runAnimation = (clips, onfinish) => { + if (isCameraMoving) return; + + const timeline = Animator.createTimeline(); + clips.forEach(timeline.addClip); + timeline.play(); + timeline.finished.then(() => { + setIsCameraMoving(false); + if (onfinish) onfinish(); + }); + setIsCameraMoving(true); + }; + + const goTop = () => { + if (isCameraMoving) return; + + const { cameraControls } = threeRef.current; + const { animations } = cameraControls; + const clip = animations.moveToTop(baseMesh); + + runAnimation(clip, () => setIsTopView(true)); + }; + + const goDown = () => { + if (isCameraMoving) return; + + const { cameraControls } = threeRef.current; + const { animations } = cameraControls; + const clip = animations.moveFromTop(data.panoramaOrigin); + + runAnimation(clip, () => setIsTopView(false)); + }; + + const eventsHandlers = useMouseSkipDrag(({ normalizedX, normalizedY }) => { + const { cameraControls } = threeRef.current; + const { animations } = cameraControls; + + const intersections = Core.raycastMeshFromScreen( + [normalizedX, normalizedY], + cameraControls.getCamera(), + baseMesh + ); + + const firstIntersections = intersections[0]; + + if (!firstIntersections) return; + + const { faceNormal, point } = firstIntersections; + const cameraHeight = data.panoramaOrigin[1]; + const target = [ + point[0] + faceNormal[0] * cameraHeight, + cameraHeight, + point[2] + faceNormal[2] * cameraHeight, + ]; + + runAnimation( + (isTopView ? animations.moveFromTop : animations.moveTo)(target), + () => setIsTopView(false) + ); + }); + return ( - - - - - - + <> + + + + + + + {!isCameraMoving && ( + + + {isTopView ? ( + + ) : ( + + )} + + + )} + ); }; diff --git a/components/Icon/index.js b/components/Icon/index.js index 57c9f69..15909ac 100644 --- a/components/Icon/index.js +++ b/components/Icon/index.js @@ -38,6 +38,8 @@ const Icon = ({ src, onClick, ...props }) => { // all svg resources is download from https://www.svgrepo.com/vectors/cursor/ const IconFolder = `/icons`; const files = { + arrowToDown: `${IconFolder}/arrowToDown.svg`, + arrowToTop: `${IconFolder}/arrowToTop.svg`, download: `${IconFolder}/download.svg`, panorama: `${IconFolder}/panorama.svg`, camera: `${IconFolder}/camera.svg`, diff --git a/docs/2d.png b/docs/2d.png new file mode 100644 index 0000000..02b3a3e Binary files /dev/null and b/docs/2d.png differ diff --git a/docs/3d.png b/docs/3d.png new file mode 100644 index 0000000..9a19d8f Binary files /dev/null and b/docs/3d.png differ diff --git a/docs/media.png b/docs/media.png new file mode 100644 index 0000000..ace3f49 Binary files /dev/null and b/docs/media.png differ diff --git a/docs/viewer.png b/docs/viewer.png new file mode 100644 index 0000000..0078e51 Binary files /dev/null and b/docs/viewer.png differ diff --git a/hooks/useMouseSkipDrag.js b/hooks/useMouseSkipDrag.js new file mode 100644 index 0000000..e070a02 --- /dev/null +++ b/hooks/useMouseSkipDrag.js @@ -0,0 +1,36 @@ +import { useState } from "react"; + +const MOUSE_UP_THRESHOLD = 5; + +const useMouseSkipDrag = (handleMouseUp) => { + const [cursorPosition, setCursorPosition] = useState(null); + const [cumulativeDelta, setCumulativeDelta] = useState(0); + + const onMouseDown = ({ offsetX, offsetY }) => { + setCursorPosition({ offsetX, offsetY }); + setCumulativeDelta(0); + }; + const onMouseMove = ({ offsetX, offsetY }) => { + if (!cursorPosition) return; + setCumulativeDelta( + (prev) => + prev + + Math.abs(offsetX - cursorPosition.offsetX) + + Math.abs(offsetY - cursorPosition.offsetY) + ); + setCursorPosition({ offsetX, offsetY }); + }; + const onMouseUp = (data) => { + setCursorPosition(null); + if (MOUSE_UP_THRESHOLD < cumulativeDelta) return; + handleMouseUp(data); + }; + + return { + onMouseDown, + onMouseMove, + onMouseUp, + }; +}; + +export default useMouseSkipDrag; diff --git a/package.json b/package.json index 1b9b3b7..4e0b312 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lint": "next lint" }, "dependencies": { + "@pano-to-mesh/anime": "1.0.0", "@pano-to-mesh/three": "1.0.0", "@pano-to-mesh/base64": "1.0.0", "next": "13.2.4", diff --git a/packages/anime/index.js b/packages/anime/index.js new file mode 100644 index 0000000..ded2605 --- /dev/null +++ b/packages/anime/index.js @@ -0,0 +1,47 @@ +import anime from "animejs"; + +const Animator = (() => { + const createTimeline = () => { + const animation = anime.timeline({ + autoplay: false, + }); + + const addClip = ({ + begin, + update, + complete, + duration, + easing, + timeOffset, + }) => { + animation.add( + { + duration: duration || 1e3, + easing: easing || "linear", + update: (anim) => { + update(anim.progress / 1e2); + }, + begin: begin, + complete: complete, + }, + timeOffset + ); + }; + + const play = () => { + animation.play(); + }; + + return { + finished: animation.finished, + addClip, + play, + }; + }; + + return { + createTimeline, + }; +})(); + +export default Animator; diff --git a/packages/anime/package.json b/packages/anime/package.json new file mode 100644 index 0000000..ed56e16 --- /dev/null +++ b/packages/anime/package.json @@ -0,0 +1,7 @@ +{ + "version": "1.0.0", + "name": "@pano-to-mesh/anime", + "dependencies": { + "animejs": "^3.2.2" + } +} diff --git a/packages/three/components/ThreeCanvas/index.js b/packages/three/components/ThreeCanvas/index.js index 5c9676a..45fd7bb 100644 --- a/packages/three/components/ThreeCanvas/index.js +++ b/packages/three/components/ThreeCanvas/index.js @@ -70,6 +70,7 @@ const ThreeCanvas = ( setThree(publicProps); const cancelResizeListener = addBeforeRenderEvent(() => { + if (!WrapperRef.current) return; const { clientWidth: width, clientHeight: height } = WrapperRef.current; setCanvasSize(width, height); css3DControls.setSize(width, height); diff --git a/packages/three/core/helpers/CameraControls.js b/packages/three/core/helpers/CameraControls.js index 08d144c..0435700 100644 --- a/packages/three/core/helpers/CameraControls.js +++ b/packages/three/core/helpers/CameraControls.js @@ -1,9 +1,62 @@ import * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; +const MODE = { + FIRST_PERSON_VIEW: "FIRST_PERSON_VIEW", + TOP_VIEW: "TOP_VIEW", +}; + +const FOCUS_VIEW_SCALE = 0.8; + +const lerp = (start, end, progress) => { + return start * (1 - progress) + end * progress; +}; + function CameraControls(camera, domElement) { const controls = new OrbitControls(camera, domElement); + const setEnable = (data) => { + controls.enabled = data; + }; + + let viewport = MODE.TOP_VIEW; + const setMode = (mode, object) => { + viewport = mode; + if (mode === MODE.FIRST_PERSON_VIEW) { + const viewDirection = new THREE.Vector3() + .subVectors(controls.target, camera.position) + .setLength(1e-6); + controls.target.addVectors(camera.position, viewDirection); + controls.maxDistance = 1e-6; + controls.minDistance = 1e-6; + controls.maxPolarAngle = Math.PI; + controls.minPolarAngle = 0; + controls.update(); + } + + if (mode === MODE.TOP_VIEW && object) { + const { distance: minDistance } = getFocusSettings(object, 1.0); + const { distance: maxDistance } = getFocusSettings(object, 0.5); + controls.maxDistance = maxDistance; + controls.minDistance = minDistance; + controls.maxPolarAngle = Math.PI / 2; + controls.minPolarAngle = 0; + controls.maxAzimuthAngle = Infinity; + controls.minAzimuthAngle = -Infinity; + controls.update(); + } + }; + + const setPosition = (target) => { + const viewDirection = new THREE.Vector3().subVectors( + controls.target, + camera.position + ); + camera.position.copy(target); + controls.target.addVectors(camera.position, viewDirection); + controls.update(); + }; + const setTarget = (target) => { const delta = new THREE.Vector3().subVectors( camera.position, @@ -38,14 +91,12 @@ function CameraControls(camera, domElement) { }; let constraintPanEvent = null; - const focus = ( - object, - constraintZoom = true, - constraintPan = true, - constraintRotate = true - ) => { + const focus = (object, constraintPan = true) => { if (!object) return; - const { origin, distance, boundingBox } = getFocusSettings(object, 0.8); + const { origin, distance, boundingBox } = getFocusSettings( + object, + FOCUS_VIEW_SCALE + ); lookAt(...origin.toArray()); controls.maxDistance = distance; @@ -53,25 +104,13 @@ function CameraControls(camera, domElement) { controls.update(); controls.maxDistance = Infinity; controls.minDistance = 0; + controls.update(); - if (constraintZoom) { - const { distance: minDistance } = getFocusSettings(object, 1.0); - const { distance: maxDistance } = getFocusSettings(object, 0.5); - controls.maxDistance = maxDistance; - controls.minDistance = minDistance; - controls.update(); - } - - if (constraintRotate) { - controls.maxPolarAngle = Math.PI / 2; - controls.minPolarAngle = 0; - controls.maxAzimuthAngle = Infinity; - controls.minAzimuthAngle = -Infinity; - controls.update(); - } + setMode(MODE.TOP_VIEW, object); if (constraintPan) { const checkTarget = () => { + if (viewport !== MODE.TOP_VIEW) return; if ( boundingBox.min.length() === Infinity || boundingBox.max.length() === Infinity @@ -107,15 +146,92 @@ function CameraControls(camera, domElement) { } }; + const moveToTop = (object) => { + const { distance } = getFocusSettings(object, FOCUS_VIEW_SCALE); + + const startDistance = controls.getDistance(); + const endDistance = distance; + + const distanceClip = { + update: (progress) => { + const targetDistance = lerp(startDistance, endDistance, progress); + controls.maxDistance = targetDistance; + controls.minDistance = targetDistance; + controls.update(); + }, + }; + + const startPolarAngle = controls.getPolarAngle(); + const endPolarAngle = Math.min(startPolarAngle, Math.PI / 4); + + const polarAngleClip = { + update: (progress) => { + const targetPolarAngle = lerp(startPolarAngle, endPolarAngle, progress); + controls.maxPolarAngle = targetPolarAngle; + controls.minPolarAngle = targetPolarAngle; + controls.update(); + }, + duration: 500, + timeOffset: 0, + }; + + const completeClip = { + update: () => {}, + complete: () => { + controls.maxDistance = endDistance; + controls.minDistance = endDistance; + controls.maxPolarAngle = endPolarAngle; + controls.minPolarAngle = endPolarAngle; + controls.update(); + setMode(MODE.TOP_VIEW, object); + }, + }; + + const clips = [distanceClip, polarAngleClip, completeClip]; + + return clips; + }; + + const moveTo = (target) => { + const start = camera.position; + const end = new THREE.Vector3().fromArray(target); + const update = (progress) => { + const position = new THREE.Vector3().lerpVectors(start, end, progress); + setPosition(position); + }; + const complete = () => setPosition(end); + + const clips = [{ update, complete }]; + + return clips; + }; + + const moveFromTop = (target) => { + const start = camera.position; + const end = new THREE.Vector3().fromArray(target); + const update = (progress) => { + const position = new THREE.Vector3().lerpVectors(start, end, progress); + setPosition(position); + }; + const begin = () => setMode(MODE.FIRST_PERSON_VIEW); + const complete = () => setPosition(end); + + const clips = [{ begin, update, complete }]; + return clips; + }; + return { domElement, getCamera: () => controls.object, - setEnable: (data) => { - controls.enabled = data; - }, + setEnable, lookAt, focus, destroy, + animations: { + moveFromTop, + moveTo, + moveToTop, + }, }; } diff --git a/packages/three/core/helpers/Raycaster.js b/packages/three/core/helpers/Raycaster.js index d7a48a0..b2af40a 100644 --- a/packages/three/core/helpers/Raycaster.js +++ b/packages/three/core/helpers/Raycaster.js @@ -26,7 +26,10 @@ export const raycastMeshFromScreen = ( const raycaster = new THREE.Raycaster(); const pointer = new THREE.Vector2(normalizedX * 2 - 1, -normalizedY * 2 + 1); raycaster.setFromCamera(pointer, camera); - const intersects = raycaster.intersectObjects(mesh, true); + const intersects = raycaster.intersectObjects( + Array.isArray(mesh) ? mesh : [mesh], + true + ); const applyWorldMatrix = (normal, object) => { const position = new THREE.Vector3(); diff --git a/public/icons/arrowToDown.svg b/public/icons/arrowToDown.svg new file mode 100644 index 0000000..996ff61 --- /dev/null +++ b/public/icons/arrowToDown.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/icons/arrowToTop.svg b/public/icons/arrowToTop.svg new file mode 100644 index 0000000..836f66a --- /dev/null +++ b/public/icons/arrowToTop.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 19c1d13..3d7cd1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1592,6 +1592,11 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +animejs@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/animejs/-/animejs-3.2.2.tgz#59be98c58834339d5847f4a70ddba74ac75b6afc" + integrity sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ== + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"