├── .browserslistrc ├── babel.config.js ├── tests └── unit │ ├── .eslintrc.js │ └── example.spec.js ├── public ├── favicon.ico └── index.html ├── src ├── assets │ └── logo.png ├── games │ ├── wx-jump-2 │ │ ├── dot.png │ │ ├── index.vue │ │ ├── defaultProp.js │ │ ├── PropCreator.js │ │ ├── Stage.js │ │ ├── utils.js │ │ ├── index.js │ │ ├── Prop.js │ │ ├── Particle.js │ │ └── LittleMan.js │ └── wx-jump-1 │ │ ├── index.vue │ │ ├── defaultProp.js │ │ ├── PropCreator.js │ │ ├── utils.js │ │ ├── Prop.js │ │ ├── Stage.js │ │ ├── index.js │ │ └── LittleMan.js ├── store.js ├── main.js ├── App.vue ├── components │ ├── MenuList.vue │ └── MenuView.vue └── router.js ├── examples ├── favicon.ico ├── css │ ├── wx-jump-stage1.e29205c6.css │ ├── wx-jump-stage2.952323e3.css │ ├── chunk-vendors.391ed6f9.css │ └── app.7834e5a2.css ├── index.html └── js │ ├── app.f1e8862e.js │ ├── wx-jump-stage1.c74513ec.js │ └── wx-jump-stage2.87b8f792.js ├── postcss.config.js ├── vue.config.js ├── README.md ├── .gitignore ├── .eslintrc.js ├── package.json └── LICENSE /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | } 5 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongj0316/jsgame100/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongj0316/jsgame100/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /examples/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongj0316/jsgame100/HEAD/examples/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: './', 3 | productionSourceMap: false 4 | } -------------------------------------------------------------------------------- /src/games/wx-jump-2/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongj0316/jsgame100/HEAD/src/games/wx-jump-2/dot.png -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | 9 | }, 10 | mutations: { 11 | 12 | }, 13 | actions: { 14 | 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsgame100 2 | 3 | [微信跳一跳 (1)](https://dongj0316.github.io/jsgame100/examples/#/wx-jump-stage1) 4 | [微信跳一跳 (2)](https://dongj0316.github.io/jsgame100/examples/#/wx-jump-stage2) 5 | 6 | ## License 7 | 8 | [MIT](https://opensource.org/licenses/MIT) 9 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import 'normalize.css' 6 | 7 | Vue.config.productionTip = false 8 | 9 | new Vue({ 10 | router, 11 | store, 12 | render: h => h(App) 13 | }).$mount('#app') 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /examples/css/wx-jump-stage1.e29205c6.css: -------------------------------------------------------------------------------- 1 | @media (max-width:768px){.jump-world[data-v-0bf23ea4]{position:fixed;top:0;left:0;width:100%;height:100%}.jump-world canvas[data-v-0bf23ea4]{width:100%;height:100%}}@media (min-width:768px){.jump-world[data-v-0bf23ea4]{width:375px;height:667px;margin:auto}.jump-world canvas[data-v-0bf23ea4]{width:100%;height:100%}} -------------------------------------------------------------------------------- /examples/css/wx-jump-stage2.952323e3.css: -------------------------------------------------------------------------------- 1 | @media (max-width:768px){.jump-world[data-v-9a0b2f7a]{position:fixed;top:0;left:0;width:100%;height:100%}.jump-world canvas[data-v-9a0b2f7a]{width:100%;height:100%}}@media (min-width:768px){.jump-world[data-v-9a0b2f7a]{width:375px;height:667px;margin:auto}.jump-world canvas[data-v-9a0b2f7a]{width:100%;height:100%}} -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { shallowMount } from '@vue/test-utils' 3 | import HelloWorld from '@/components/HelloWorld.vue' 4 | 5 | describe('HelloWorld.vue', () => { 6 | it('renders props.msg when passed', () => { 7 | const msg = 'new message' 8 | const wrapper = shallowMount(HelloWorld, { 9 | propsData: { msg } 10 | }) 11 | expect(wrapper.text()).to.include(msg) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | }, 17 | overrides: [ 18 | { 19 | files: [ 20 | '**/__tests__/*.{j,t}s?(x)' 21 | ], 22 | env: { 23 | mocha: true 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | jsgame100 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/games/wx-jump-1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /src/games/wx-jump-2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /src/games/wx-jump-1/defaultProp.js: -------------------------------------------------------------------------------- 1 | import { 2 | randomArrayElm, 3 | rangeNumberInclusive 4 | } from './utils' 5 | 6 | let cid = 0 7 | const colors = [0x67C23A, 0xE6A23C, 0xF56C6C, 0x909399, 0x409EFF, 0xffffff] 8 | 9 | // 静态 10 | export const statics = [ 11 | // ... 12 | ] 13 | 14 | // 非静态 15 | export const actives = [ 16 | // 默认纯色立方体创造器 17 | function defaultCreator (THREE, helpers) { 18 | const { 19 | propSizeRange: [min, max], 20 | propHeight, 21 | baseMeshLambertMaterial, 22 | baseBoxBufferGeometry 23 | } = helpers 24 | 25 | ++cid 26 | 27 | // 随机颜色 28 | const color = randomArrayElm(colors) 29 | // 随机大小,头2个盒子固定一下大小 30 | const size = cid < 3 ? max : rangeNumberInclusive(min, max) 31 | 32 | const geometry = baseBoxBufferGeometry.clone() 33 | geometry.scale(size, propHeight, size) 34 | 35 | const material = baseMeshLambertMaterial.clone() 36 | material.setValues({ color }) 37 | 38 | return new THREE.Mesh(geometry, material) 39 | }, 40 | ] -------------------------------------------------------------------------------- /src/components/MenuList.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsgame100", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "build-examples": "vue-cli-service build --dest examples", 9 | "lint": "vue-cli-service lint", 10 | "test:unit": "vue-cli-service test:unit" 11 | }, 12 | "dependencies": { 13 | "@tweenjs/tween.js": "^18.3.1", 14 | "core-js": "^2.6.5", 15 | "normalize.css": "^8.0.1", 16 | "three": "^0.109.0", 17 | "vue": "^2.6.10", 18 | "vue-router": "^3.0.3", 19 | "vuex": "^3.0.1" 20 | }, 21 | "devDependencies": { 22 | "@vue/cli-plugin-babel": "^3.12.0", 23 | "@vue/cli-plugin-eslint": "^3.12.0", 24 | "@vue/cli-plugin-unit-mocha": "^3.12.0", 25 | "@vue/cli-service": "^3.12.0", 26 | "@vue/test-utils": "1.0.0-beta.29", 27 | "babel-eslint": "^10.0.1", 28 | "chai": "^4.1.2", 29 | "eslint": "^5.16.0", 30 | "eslint-plugin-vue": "^5.0.0", 31 | "less": "^3.0.4", 32 | "less-loader": "^5.0.0", 33 | "vue-template-compiler": "^2.6.10" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | jsgame100
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 dongj0316 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import MenuList from '@/components/MenuList' 4 | import MenuView from '@/components/MenuView' 5 | 6 | Vue.use(Router) 7 | 8 | const withNameView = (name, view, optons) => { 9 | const { component, ...otherOptions } = optons 10 | 11 | return { 12 | components: { 13 | default: component, 14 | [name || view.name]: view 15 | }, 16 | ...otherOptions 17 | } 18 | } 19 | 20 | export const withMenuViewRoutes = [ 21 | { 22 | path: '/wx-jump-stage1', 23 | name: 'wx-jump-stage1', 24 | meta: { 25 | title: '微信跳一跳 (1)' 26 | }, 27 | component: () => import(/* webpackChunkName: "wx-jump-stage1" */ './games/wx-jump-1/index.vue') 28 | }, 29 | { 30 | path: '/wx-jump-stage2', 31 | name: 'wx-jump-stage2', 32 | meta: { 33 | title: '微信跳一跳 (2)' 34 | }, 35 | component: () => import(/* webpackChunkName: "wx-jump-stage2" */ './games/wx-jump-2/index.vue') 36 | } 37 | ] 38 | 39 | const defaultRoutes = [ 40 | { 41 | path: '/', 42 | name: 'menu-list', 43 | component: MenuList 44 | } 45 | ] 46 | 47 | export default new Router({ 48 | routes: [ 49 | ...defaultRoutes, 50 | ...withMenuViewRoutes.map(withNameView.bind(null, 'MenuView', MenuView)), 51 | { 52 | path: '*', 53 | component: MenuList 54 | } 55 | ] 56 | }) 57 | -------------------------------------------------------------------------------- /src/games/wx-jump-2/defaultProp.js: -------------------------------------------------------------------------------- 1 | import { 2 | randomArrayElm, 3 | rangeNumberInclusive, 4 | propCounter, 5 | incrementPropCounter, 6 | colors 7 | } from './utils' 8 | 9 | // 静态 10 | export const statics = [ 11 | // ... 12 | ] 13 | 14 | // 非静态 15 | export const actives = [ 16 | // 默认纯色立方体创造器 17 | function defaultCreator (THREE, helpers) { 18 | const { 19 | propSizeRange: [min, max], 20 | propHeight, 21 | baseMeshLambertMaterial, 22 | baseBoxBufferGeometry 23 | } = helpers 24 | 25 | incrementPropCounter() 26 | 27 | // 随机颜色 28 | const color = randomArrayElm(colors) 29 | // 随机大小,头2个盒子固定一下大小 30 | const size = propCounter < 3 ? max : rangeNumberInclusive(min, max) 31 | 32 | const geometry = baseBoxBufferGeometry.clone() 33 | geometry.scale(size, propHeight, size) 34 | 35 | const material = baseMeshLambertMaterial.clone() 36 | material.setValues({ color }) 37 | 38 | return new THREE.Mesh(geometry, material) 39 | }, 40 | function defaultCreator (THREE, helpers) { 41 | const { 42 | propSizeRange: [min, max], 43 | propHeight, 44 | baseMeshLambertMaterial, 45 | baseCylinderBufferGeometry 46 | } = helpers 47 | 48 | incrementPropCounter() 49 | 50 | // 随机颜色 51 | const color = randomArrayElm(colors) 52 | // 随机大小,头2个盒子固定一下大小 53 | const size = propCounter < 3 ? max : rangeNumberInclusive(min, max) 54 | 55 | const geometry = baseCylinderBufferGeometry.clone() 56 | geometry.scale(Math.ceil(size / 2), propHeight, Math.ceil(size / 2)) 57 | 58 | const material = baseMeshLambertMaterial.clone() 59 | material.setValues({ color }) 60 | 61 | return new THREE.Mesh(geometry, material) 62 | }, 63 | ] -------------------------------------------------------------------------------- /examples/css/chunk-vendors.391ed6f9.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none} -------------------------------------------------------------------------------- /src/games/wx-jump-1/PropCreator.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { 3 | baseMeshLambertMaterial, 4 | baseBoxBufferGeometry, 5 | randomArrayElm, 6 | rangeNumberInclusive 7 | } from './utils' 8 | import { statics, actives } from './defaultProp' 9 | 10 | class PropCreator { 11 | constructor ({ 12 | propHeight, 13 | propSizeRange, 14 | needDefaultCreator 15 | }) { 16 | this.propHeight = propHeight 17 | this.propSizeRange = propSizeRange 18 | 19 | // 维护的创造器 20 | this.propCreators = [] 21 | 22 | if (needDefaultCreator) { 23 | this.createPropCreator(actives, false) 24 | this.createPropCreator(statics, true) 25 | } 26 | } 27 | 28 | createProp (index) { 29 | const { propCreators } = this 30 | return index > -1 31 | ? propCreators[index] && propCreators[index]() || randomArrayElm(propCreators)() 32 | : randomArrayElm(propCreators)() 33 | } 34 | 35 | /** 36 | * 新增定制化的创造器 37 | * @param {Function} creator 创造器函数 38 | * @param {Boolean} isStatic 是否是动态创建 39 | */ 40 | createPropCreator (creator, isStatic) { 41 | if (Array.isArray(creator)) { 42 | creator.forEach(crt => this.createPropCreator(crt, isStatic)) 43 | return 44 | } 45 | 46 | const { propCreators, propSizeRange, propHeight } = this 47 | 48 | if (propCreators.indexOf(creator) > -1) { 49 | return 50 | } 51 | 52 | const wrappedCreator = function () { 53 | if (isStatic && wrappedCreator.box) { 54 | // 静态盒子,下次直接clone 55 | return wrappedCreator.box.clone() 56 | } else { 57 | const box = creator(THREE, { 58 | propSizeRange, 59 | propHeight, 60 | baseMeshLambertMaterial, 61 | baseBoxBufferGeometry 62 | }) 63 | 64 | if (isStatic) { 65 | // 被告知是静态盒子,缓存起来 66 | wrappedCreator.box = box 67 | } 68 | return box 69 | } 70 | } 71 | 72 | propCreators.push(wrappedCreator) 73 | } 74 | } 75 | 76 | export default PropCreator -------------------------------------------------------------------------------- /src/games/wx-jump-2/PropCreator.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { 3 | baseMeshLambertMaterial, 4 | baseBoxBufferGeometry, 5 | baseCylinderBufferGeometry, 6 | randomArrayElm, 7 | rangeNumberInclusive 8 | } from './utils' 9 | import { statics, actives } from './defaultProp' 10 | 11 | class PropCreator { 12 | constructor ({ 13 | propHeight, 14 | propSizeRange, 15 | needDefaultCreator 16 | }) { 17 | this.propHeight = propHeight 18 | this.propSizeRange = propSizeRange 19 | 20 | // 维护的创造器 21 | this.propCreators = [] 22 | 23 | if (needDefaultCreator) { 24 | this.createPropCreator(actives, false) 25 | this.createPropCreator(statics, true) 26 | } 27 | } 28 | 29 | createProp (index) { 30 | const { propCreators } = this 31 | return index > -1 32 | ? propCreators[index] && propCreators[index]() || randomArrayElm(propCreators)() 33 | : randomArrayElm(propCreators)() 34 | } 35 | 36 | /** 37 | * 新增定制化的创造器 38 | * @param {Function} creator 创造器函数 39 | * @param {Boolean} isStatic 是否是动态创建 40 | */ 41 | createPropCreator (creator, isStatic) { 42 | if (Array.isArray(creator)) { 43 | creator.forEach(crt => this.createPropCreator(crt, isStatic)) 44 | return 45 | } 46 | 47 | const { propCreators, propSizeRange, propHeight } = this 48 | 49 | if (propCreators.indexOf(creator) > -1) { 50 | return 51 | } 52 | 53 | const wrappedCreator = function () { 54 | if (isStatic && wrappedCreator.box) { 55 | // 静态盒子,下次直接clone 56 | return wrappedCreator.box.clone() 57 | } else { 58 | const box = creator(THREE, { 59 | propSizeRange, 60 | propHeight, 61 | baseMeshLambertMaterial, 62 | baseBoxBufferGeometry, 63 | baseCylinderBufferGeometry 64 | }) 65 | 66 | if (isStatic) { 67 | // 被告知是静态盒子,缓存起来 68 | wrappedCreator.box = box 69 | } 70 | return box 71 | } 72 | } 73 | 74 | propCreators.push(wrappedCreator) 75 | } 76 | } 77 | 78 | export default PropCreator -------------------------------------------------------------------------------- /examples/css/app.7834e5a2.css: -------------------------------------------------------------------------------- 1 | *{-webkit-box-sizing:border-box;box-sizing:border-box}#app,body,html{width:100%}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#2c3e50;overflow-x:hidden;overflow-y:auto}.menu-list[data-v-7aa4251c]{padding:30px;margin:0;list-style:none;text-align:center}.menu-list h1[data-v-7aa4251c]{margin-top:0;font-size:24px}.menu-list li[data-v-7aa4251c]{padding:5px 0}.menu-list a[data-v-7aa4251c]{font-weight:700;color:#2c3e50}.menu-list a.router-link-exact-active[data-v-7aa4251c]{color:#42b983}.menu-view-list[data-v-9a5b6090]{position:fixed;top:0;left:0;height:100%;width:220px;-webkit-box-shadow:0 0 15px #808b96;box-shadow:0 0 15px #808b96;overflow-x:hidden;overflow-y:auto;-ms-scroll-chaining:none;overscroll-behavior:contain;-webkit-transform:translateZ(0);transform:translateZ(0);background:#fff;z-index:5000}.menu-view-mask[data-v-9a5b6090]{position:fixed;top:0;left:0;right:0;bottom:0;z-index:4999}.menu-view-trigger[data-v-9a5b6090]{position:fixed;top:20px;left:20px;z-index:5001}.menu-view-trigger button[data-v-9a5b6090]{width:44px;height:44px;border-radius:50%;-webkit-box-shadow:0 0 15px #808b96;box-shadow:0 0 15px #808b96;border:none;background:rgba(44,62,80,.5)}@media (max-width:768px){.menu-view-list[data-v-9a5b6090]{width:70vw}}.menu-view-trigger-slide-enter-active[data-v-9a5b6090]{-webkit-animation:slide-bottom-data-v-9a5b6090 .3s;animation:slide-bottom-data-v-9a5b6090 .3s}.menu-view-trigger-slide-leave-active[data-v-9a5b6090]{-webkit-animation:slide-top-data-v-9a5b6090 .3s .3s;animation:slide-top-data-v-9a5b6090 .3s .3s}.menu-view-slide-enter-active[data-v-9a5b6090]{-webkit-animation:slide-right-data-v-9a5b6090 .3s;animation:slide-right-data-v-9a5b6090 .3s}.menu-view-slide-leave-active[data-v-9a5b6090]{-webkit-animation:slide-left-data-v-9a5b6090 .3s;animation:slide-left-data-v-9a5b6090 .3s}@-webkit-keyframes slide-right-data-v-9a5b6090{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slide-right-data-v-9a5b6090{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@-webkit-keyframes slide-left-data-v-9a5b6090{to{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slide-left-data-v-9a5b6090{to{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@-webkit-keyframes slide-bottom-data-v-9a5b6090{0%{-webkit-transform:translate3d(0,-200%,0);transform:translate3d(0,-200%,0)}}@keyframes slide-bottom-data-v-9a5b6090{0%{-webkit-transform:translate3d(0,-200%,0);transform:translate3d(0,-200%,0)}}@-webkit-keyframes slide-top-data-v-9a5b6090{to{-webkit-transform:translate3d(0,-200%,0);transform:translate3d(0,-200%,0)}}@keyframes slide-top-data-v-9a5b6090{to{-webkit-transform:translate3d(0,-200%,0);transform:translate3d(0,-200%,0)}} -------------------------------------------------------------------------------- /src/components/MenuView.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | 40 | -------------------------------------------------------------------------------- /src/games/wx-jump-1/utils.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import TWEEN from '@tweenjs/tween.js' 3 | 4 | const { random, sqrt, floor, pow, sin, cos, tan, PI } = Math 5 | 6 | /** 7 | * 根据角度计算相机初始位置 8 | * @param {Number} verticalDeg 相机和场景中心点的垂直角度 9 | * @param {Number} horizontalDeg 相机和x轴的水平角度 10 | * @param {Number} top 相机上侧面 11 | * @param {Number} bottom 相机下侧面 12 | * @param {Number} near 摄像机视锥体近端面 13 | * @param {Number} far 摄像机视锥体远端面 14 | */ 15 | export function computeCameraInitalPosition (verticalDeg, horizontalDeg, top, bottom, near, far) { 16 | const verticalRadian = verticalDeg * (PI / 180) 17 | const horizontalRadian = horizontalDeg * (PI / 180) 18 | const minY = cos(verticalRadian) * bottom 19 | const maxY = sin(verticalRadian) * (far - near - top / tan(verticalRadian)) 20 | 21 | if (minY > maxY) { 22 | console.warn('警告: 垂直角度太小了!') 23 | return 24 | } 25 | const y = minY + (maxY - minY) / 2 26 | const longEdge = y / tan(verticalRadian) 27 | const x = sin(horizontalRadian) * longEdge 28 | const z = cos(horizontalRadian) * longEdge 29 | 30 | return { x, y, z } 31 | } 32 | 33 | // 材质 34 | export const baseMeshLambertMaterial = new THREE.MeshLambertMaterial() 35 | // 立方体 36 | export const baseBoxBufferGeometry = new THREE.BoxBufferGeometry() 37 | 38 | export const randomArrayElm = array => array[floor(random() * array.length)] 39 | 40 | export const rangeNumberInclusive = (min, max) => floor(random() * (max - min + 1)) + min 41 | 42 | export const getPropSize = box => { 43 | const box3 = getPropSize.box3 || (getPropSize.box3 = new THREE.Box3()) 44 | box3.setFromObject(box) 45 | return box3.getSize(new THREE.Vector3()) 46 | } 47 | 48 | // 斜抛计算 49 | export const computeObligueThrowValue = function (v0, theta, G) { 50 | const sin2θ = sin(2 * theta) 51 | const sinθ = sin(theta) 52 | 53 | const rangeR = pow(v0, 2) * sin2θ / G 54 | const rangeH = pow(v0 * sinθ, 2) / (2 * G) 55 | // const rangeT = 2 * v0 * sinθ / G 56 | 57 | return { 58 | rangeR, 59 | rangeH, 60 | // rangeT 61 | } 62 | } 63 | 64 | /** 65 | * 根据射程算出落地点 66 | * @param {Number} range 射程 67 | * @param {Object} c1 起跳点 68 | * @param {Object} p2 目标盒子中心点 69 | */ 70 | export const computePositionByRangeR = function (range, c1, p2) { 71 | const { x: c1x, z: c1z } = c1 72 | const { x: p2x, z: p2z } = p2 73 | 74 | const p2cx = p2x - c1x 75 | const p2cz = p2z - c1z 76 | const p2c = sqrt(pow(p2cz, 2) + pow(p2cx, 2)) 77 | 78 | const jumpDownX = p2cx * range / p2c 79 | const jumpDownZ = p2cz * range / p2c 80 | 81 | return { 82 | jumpDownX: c1x + jumpDownX, 83 | jumpDownZ: c1z + jumpDownZ 84 | } 85 | } 86 | 87 | export const animate = (configs, onUpdate, onComplete) => { 88 | const { 89 | from, to, duration, 90 | easing = k => k, 91 | autoStart = true // 为了使用tween的chain 92 | } = configs 93 | 94 | const tween = new TWEEN.Tween(from) 95 | .to(to, duration) 96 | .easing(easing) 97 | .onUpdate(onUpdate) 98 | .onComplete(() => { 99 | onComplete && onComplete() 100 | }) 101 | 102 | if (autoStart) { 103 | tween.start() 104 | } 105 | 106 | animateFrame() 107 | return tween 108 | } 109 | 110 | const animateFrame = function () { 111 | if (animateFrame.openin) { 112 | return 113 | } 114 | animateFrame.openin = true 115 | 116 | const animate = () => { 117 | const id = requestAnimationFrame(animate) 118 | if (!TWEEN.update()) { 119 | animateFrame.openin = false 120 | cancelAnimationFrame(id) 121 | } 122 | } 123 | animate() 124 | } -------------------------------------------------------------------------------- /src/games/wx-jump-1/Prop.js: -------------------------------------------------------------------------------- 1 | import TWEEN from '@tweenjs/tween.js' 2 | import { getPropSize, rangeNumberInclusive, animate } from './utils' 3 | 4 | class Prop { 5 | constructor ({ 6 | world, // 所处世界 7 | stage, // 所处舞台 8 | body, // 主体 9 | height, 10 | enterHeight, 11 | distanceRange, 12 | prev 13 | }) { 14 | this.world = world 15 | this.stage = stage 16 | this.body = body 17 | this.height = height 18 | this.enterHeight = enterHeight 19 | this.distanceRange = distanceRange 20 | this.prev = prev 21 | } 22 | 23 | // 计算位置 24 | computeMyPosition () { 25 | const { 26 | world, 27 | prev, 28 | distanceRange, 29 | enterHeight 30 | } = this 31 | const position = { 32 | x: 0, 33 | // 头2个盒子y值为0 34 | y: enterHeight, 35 | z: 0 36 | } 37 | 38 | if (!prev) { 39 | // 第1个盒子 40 | return position 41 | } 42 | 43 | if (enterHeight === 0) { 44 | // 第2个盒子,固定一个距离 45 | position.z = world.width / 2 46 | return position 47 | } 48 | 49 | const { x, z } = prev.getPosition() 50 | // 随机2个方向 x or z 51 | const direction = Math.round(Math.random()) === 0 52 | const { x: prevWidth, z: prevDepth } = prev.getSize() 53 | const { x: currentWidth, z: currentDepth } = this.getSize() 54 | // 根据区间随机一个距离 55 | const randomDistance = rangeNumberInclusive(...distanceRange) 56 | 57 | if (direction) { 58 | position.x = x + prevWidth / 2 + randomDistance + currentWidth / 2 59 | position.z = z 60 | } else { 61 | position.x = x 62 | position.z = z + prevDepth / 2 + randomDistance + currentDepth / 2 63 | } 64 | 65 | return position 66 | } 67 | 68 | // 将道具放入舞台 69 | enterStage () { 70 | const { stage, body, height } = this 71 | const { x, y, z } = this.computeMyPosition() 72 | 73 | body.castShadow = true 74 | body.receiveShadow = true 75 | body.position.set(x, y, z) 76 | // 需要将盒子放到地面 77 | body.geometry.translate(0, height / 2, 0) 78 | 79 | stage.add(body) 80 | stage.render() 81 | 82 | this.entranceTransition() 83 | } 84 | 85 | // 盒子的入场动画 86 | entranceTransition (duration = 400) { 87 | const { body, enterHeight, stage } = this 88 | 89 | if (enterHeight === 0) { 90 | return 91 | } 92 | 93 | animate( 94 | { 95 | to: { y: 0 }, 96 | from: { y: enterHeight }, 97 | duration, 98 | easing: TWEEN.Easing.Bounce.Out 99 | }, 100 | ({ y }) => { 101 | body.position.setY(y) 102 | stage.render() 103 | } 104 | ) 105 | } 106 | 107 | // 回弹动画 108 | springbackTransition (duration) { 109 | const { body, stage } = this 110 | const y = body.scale.y 111 | 112 | animate( 113 | { 114 | from: { y }, 115 | to: { y: 1 }, 116 | duration, 117 | easing: TWEEN.Easing.Bounce.Out 118 | }, 119 | ({ y }) => { 120 | body.scale.setY(y) 121 | stage.render() 122 | } 123 | ) 124 | } 125 | 126 | setNext (next) { 127 | this.next = next 128 | } 129 | 130 | getNext (next) { 131 | return this.next 132 | } 133 | 134 | getPosition () { 135 | return this.body.position 136 | } 137 | 138 | setPosition (x, y, z) { 139 | return this.body.position.set(x, y, z) 140 | } 141 | 142 | scaleY (y) { 143 | return this.body.scale.setY(y) 144 | } 145 | 146 | // 获取道具大小 147 | getSize () { 148 | return getPropSize(this.body) 149 | } 150 | 151 | // 销毁 152 | dispose () { 153 | const { body, stage, prev, next } = this 154 | // 解除关联的引用 155 | this.prev = null 156 | this.next = null 157 | if (prev) { 158 | prev.next = null 159 | } 160 | if (next) { 161 | next.prev = null 162 | } 163 | 164 | body.geometry.dispose() 165 | body.material.dispose() 166 | stage.remove(body) 167 | } 168 | } 169 | 170 | export default Prop -------------------------------------------------------------------------------- /src/games/wx-jump-1/Stage.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import TWEEN from '@tweenjs/tween.js' 3 | import { baseMeshLambertMaterial, animate } from './utils' 4 | 5 | class Stage { 6 | constructor ({ 7 | width, 8 | height, 9 | canvas, 10 | axesHelper = false, // 辅助线 11 | cameraNear, // 相机近截面 12 | cameraFar, // 相机远截面 13 | cameraInitalPosition, // 相机初始位置 14 | lightInitalPosition // 光源初始位置 15 | }) { 16 | this.width = width 17 | this.height = height 18 | this.canvas = canvas 19 | this.axesHelper = axesHelper 20 | // 正交相机配置 21 | this.cameraNear = cameraNear 22 | this.cameraFar = cameraFar 23 | this.cameraInitalPosition = cameraInitalPosition 24 | this.lightInitalPosition = lightInitalPosition 25 | 26 | this.scene = null 27 | this.plane = null 28 | this.light = null 29 | this.camera = null 30 | this.renderer = null 31 | 32 | this.init() 33 | } 34 | 35 | init () { 36 | this.createScene() 37 | this.createPlane() 38 | this.createLight() 39 | this.createCamera() 40 | this.createRenterer() 41 | this.render() 42 | } 43 | 44 | // 场景 45 | createScene () { 46 | const scene = this.scene = new THREE.Scene() 47 | scene.background = new THREE.Color(0xd6dbdf) 48 | 49 | if (this.axesHelper) { 50 | scene.add(new THREE.AxesHelper(10e3)) 51 | } 52 | } 53 | 54 | // 地面 55 | createPlane () { 56 | const { scene } = this 57 | const geometry = new THREE.PlaneBufferGeometry(10e2, 10e2, 1, 1) 58 | const meterial = new THREE.ShadowMaterial() 59 | meterial.opacity = 0.5 60 | 61 | const plane = this.plane = new THREE.Mesh(geometry, meterial) 62 | 63 | plane.rotation.x = -.5 * Math.PI 64 | plane.position.y = -.1 65 | // 接收阴影 66 | plane.receiveShadow = true 67 | scene.add(plane) 68 | } 69 | 70 | // 光 71 | createLight () { 72 | const { scene, lightInitalPosition: { x, y, z }, height } = this 73 | const light = this.light = new THREE.DirectionalLight(0xffffff, .5) 74 | const lightTarget = this.lightTarget = new THREE.Object3D() 75 | 76 | light.target = lightTarget 77 | light.position.set(x, y, z) 78 | // 开启阴影投射 79 | light.castShadow = true 80 | // // 定义可见域的投射阴影 81 | light.shadow.camera.left = -height 82 | light.shadow.camera.right = height 83 | light.shadow.camera.top = height 84 | light.shadow.camera.bottom = -height 85 | light.shadow.camera.near = 0 86 | light.shadow.camera.far = 2000 87 | // 定义阴影的分辨率 88 | light.shadow.mapSize.width = 1600 89 | light.shadow.mapSize.height = 1600 90 | 91 | // 环境光 92 | scene.add(new THREE.AmbientLight(0xE5E7E9, .4)) 93 | scene.add(new THREE.HemisphereLight(0xffffff, 0xffffff, .2)) 94 | scene.add(lightTarget) 95 | scene.add(light) 96 | } 97 | 98 | // 相机 99 | createCamera () { 100 | const { 101 | scene, 102 | width, height, 103 | cameraInitalPosition: { x, y, z }, 104 | cameraNear, cameraFar 105 | } = this 106 | const camera = this.camera = new THREE.OrthographicCamera(-width / 2, width / 2, height / 2, -height / 2, cameraNear, cameraFar) 107 | 108 | camera.position.set(x, y, z) 109 | camera.lookAt(scene.position) 110 | scene.add(camera) 111 | } 112 | 113 | // 渲染器 114 | createRenterer () { 115 | const { canvas, width, height } = this 116 | const renderer = this.renderer = new THREE.WebGLRenderer({ 117 | canvas, 118 | alpha: true, // 透明场景 119 | antialias:true // 抗锯齿 120 | }) 121 | 122 | renderer.setSize(width, height) 123 | // 开启阴影 124 | renderer.shadowMap.enabled = true 125 | // 设置设备像素 126 | renderer.setPixelRatio(window.devicePixelRatio) 127 | } 128 | 129 | // 执行渲染 130 | render () { 131 | const { scene, camera } = this 132 | this.renderer.render(scene, camera) 133 | } 134 | 135 | // center为2个盒子的中心点 136 | moveCamera ({ cameraTo, center, lightTo }, onComplete, duration) { 137 | const { 138 | camera, plane, 139 | light, lightTarget, 140 | lightInitalPosition 141 | } = this 142 | 143 | // 移动相机 144 | animate( 145 | { 146 | from: { ...camera.position }, 147 | to: cameraTo, 148 | duration 149 | }, 150 | ({ x, y, z }) => { 151 | camera.position.x = x 152 | camera.position.z = z 153 | this.render() 154 | }, 155 | onComplete 156 | ) 157 | 158 | // 灯光和目标也需要动起来,为了保证阴影位置不变 159 | const { x: lightInitalX, z: lightInitalZ } = lightInitalPosition 160 | animate( 161 | { 162 | from: { ...light.position }, 163 | to: lightTo, 164 | duration 165 | }, 166 | ({ x, y, z }) => { 167 | lightTarget.position.x = x - lightInitalX 168 | lightTarget.position.z = z - lightInitalZ 169 | light.position.set(x, y, z) 170 | } 171 | ) 172 | 173 | // 保证不会跑出有限大小的地面 174 | plane.position.x = center.x 175 | plane.position.z = center.z 176 | } 177 | 178 | // 场景中添加物体 179 | add (...args) { 180 | return this.scene.add(...args) 181 | } 182 | // 移除场景中的物体 183 | remove (...args) { 184 | return this.scene.remove(...args) 185 | } 186 | } 187 | 188 | export default Stage -------------------------------------------------------------------------------- /src/games/wx-jump-2/Stage.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { animate, destroyMesh } from './utils' 3 | 4 | class Stage { 5 | constructor ({ 6 | width, 7 | height, 8 | canvas, 9 | axesHelper = false, // 辅助线 10 | cameraNear, // 相机近截面 11 | cameraFar, // 相机远截面 12 | cameraInitalPosition, // 相机初始位置 13 | lightInitalPosition // 光源初始位置 14 | }) { 15 | this.width = width 16 | this.height = height 17 | this.canvas = canvas 18 | this.axesHelper = axesHelper 19 | // 正交相机配置 20 | this.cameraNear = cameraNear 21 | this.cameraFar = cameraFar 22 | this.cameraInitalPosition = cameraInitalPosition 23 | this.lightInitalPosition = lightInitalPosition 24 | 25 | this.scene = null 26 | this.plane = null 27 | this.light = null 28 | this.camera = null 29 | this.renderer = null 30 | 31 | this.init() 32 | } 33 | 34 | init () { 35 | this.createScene() 36 | this.createPlane() 37 | this.createLight() 38 | this.createCamera() 39 | this.createRenterer() 40 | this.render() 41 | } 42 | 43 | // 场景销毁 44 | destroy () { 45 | destroyMesh(this.camera) 46 | destroyMesh(this.light) 47 | destroyMesh(this.plane) 48 | 49 | this.scene.remove(this.camera) 50 | this.scene.remove(this.light) 51 | this.scene.remove(this.plane) 52 | this.scene.children.forEach(destroyMesh) 53 | this.scene.children = null 54 | 55 | this.scene.dispose() 56 | this.renderer.dispose() 57 | 58 | this.canvas = null 59 | this.scene = null 60 | this.plane = null 61 | this.light = null 62 | this.camera = null 63 | this.renderer = null 64 | } 65 | 66 | // 重置场景到初始状态 67 | reset () { 68 | const { 69 | plane, 70 | light, lightTarget, lightInitalPosition, 71 | camera, cameraInitalPosition 72 | } = this 73 | plane.position.x = 0 74 | plane.position.z = 0 75 | lightTarget.position.x = 0 76 | lightTarget.position.z = 0 77 | light.position.set(lightInitalPosition.x, lightInitalPosition.y, lightInitalPosition.z) 78 | camera.position.x = cameraInitalPosition.x 79 | camera.position.z = cameraInitalPosition.z 80 | } 81 | 82 | // 场景 83 | createScene () { 84 | const scene = this.scene = new THREE.Scene() 85 | scene.background = new THREE.Color(0xd6dbdf) 86 | 87 | if (this.axesHelper) { 88 | scene.add(new THREE.AxesHelper(10e3)) 89 | } 90 | } 91 | 92 | // 地面 93 | createPlane () { 94 | const { scene } = this 95 | const geometry = new THREE.PlaneBufferGeometry(10e2, 10e2, 1, 1) 96 | const meterial = new THREE.ShadowMaterial() 97 | meterial.opacity = 0.5 98 | 99 | const plane = this.plane = new THREE.Mesh(geometry, meterial) 100 | 101 | plane.rotation.x = -.5 * Math.PI 102 | plane.position.y = -.1 103 | // 接收阴影 104 | plane.receiveShadow = true 105 | scene.add(plane) 106 | } 107 | 108 | // 光 109 | createLight () { 110 | const { scene, lightInitalPosition: { x, y, z }, height } = this 111 | const light = this.light = new THREE.DirectionalLight(0xffffff, .5) 112 | const lightTarget = this.lightTarget = new THREE.Object3D() 113 | 114 | light.target = lightTarget 115 | light.position.set(x, y, z) 116 | // 开启阴影投射 117 | light.castShadow = true 118 | // // 定义可见域的投射阴影 119 | light.shadow.camera.left = -height 120 | light.shadow.camera.right = height 121 | light.shadow.camera.top = height 122 | light.shadow.camera.bottom = -height 123 | light.shadow.camera.near = 0 124 | light.shadow.camera.far = 2000 125 | // 定义阴影的分辨率 126 | light.shadow.mapSize.width = 1600 127 | light.shadow.mapSize.height = 1600 128 | 129 | // 环境光 130 | scene.add(new THREE.AmbientLight(0xE5E7E9, .4)) 131 | scene.add(new THREE.HemisphereLight(0xffffff, 0xffffff, .2)) 132 | scene.add(lightTarget) 133 | scene.add(light) 134 | } 135 | 136 | // 相机 137 | createCamera () { 138 | const { 139 | scene, 140 | width, height, 141 | cameraInitalPosition: { x, y, z }, 142 | cameraNear, cameraFar 143 | } = this 144 | const camera = this.camera = new THREE.OrthographicCamera(-width / 2, width / 2, height / 2, -height / 2, cameraNear, cameraFar) 145 | 146 | camera.position.set(x, y, z) 147 | camera.lookAt(scene.position) 148 | scene.add(camera) 149 | } 150 | 151 | // 渲染器 152 | createRenterer () { 153 | const { canvas, width, height } = this 154 | const renderer = this.renderer = new THREE.WebGLRenderer({ 155 | canvas, 156 | alpha: true, // 透明场景 157 | antialias:true // 抗锯齿 158 | }) 159 | 160 | renderer.setSize(width, height) 161 | // 开启阴影 162 | renderer.shadowMap.enabled = true 163 | // 设置设备像素 164 | renderer.setPixelRatio(window.devicePixelRatio) 165 | } 166 | 167 | // 执行渲染 168 | render () { 169 | const { scene, camera } = this 170 | this.renderer.render(scene, camera) 171 | } 172 | 173 | // center为2个盒子的中心点 174 | moveCamera ({ cameraTo, center, lightTo }, onComplete, duration) { 175 | const { 176 | camera, plane, 177 | light, lightTarget, 178 | lightInitalPosition 179 | } = this 180 | 181 | // 移动相机 182 | animate( 183 | { 184 | from: { ...camera.position }, 185 | to: cameraTo, 186 | duration 187 | }, 188 | ({ x, z }) => { 189 | camera.position.x = x 190 | camera.position.z = z 191 | this.render() 192 | }, 193 | onComplete 194 | ) 195 | 196 | // 灯光和目标也需要动起来,为了保证阴影位置不变 197 | const { x: lightInitalX, z: lightInitalZ } = lightInitalPosition 198 | animate( 199 | { 200 | from: { ...light.position }, 201 | to: lightTo, 202 | duration 203 | }, 204 | ({ x, y, z }) => { 205 | lightTarget.position.x = x - lightInitalX 206 | lightTarget.position.z = z - lightInitalZ 207 | light.position.set(x, y, z) 208 | } 209 | ) 210 | 211 | // 保证不会跑出有限大小的地面 212 | plane.position.x = center.x 213 | plane.position.z = center.z 214 | } 215 | 216 | // 场景中添加物体 217 | add (...args) { 218 | return this.scene.add(...args) 219 | } 220 | // 移除场景中的物体 221 | remove (...args) { 222 | return this.scene.remove(...args) 223 | } 224 | } 225 | 226 | export default Stage -------------------------------------------------------------------------------- /src/games/wx-jump-1/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | computeCameraInitalPosition, 3 | getPropSize, 4 | rangeNumberInclusive, 5 | animate 6 | } from './utils' 7 | import TWEEN from '@tweenjs/tween.js' 8 | import Stage from './Stage' 9 | import PropCreator from './PropCreator' 10 | import Prop from './Prop' 11 | import LittleMan from './LittleMan' 12 | 13 | class JumpGameWorld { 14 | constructor ({ 15 | container, 16 | canvas, 17 | needDefaultCreator = true, 18 | axesHelper = false 19 | }) { 20 | const { offsetWidth, offsetHeight } = container 21 | this.G = 9.8 22 | this.container = container 23 | this.canvas = canvas 24 | this.width = offsetWidth 25 | this.height = offsetHeight 26 | this.needDefaultCreator = needDefaultCreator 27 | this.axesHelper = axesHelper 28 | 29 | const [min, max] = [~~(offsetWidth / 6), ~~(offsetWidth / 3.5)] 30 | this.propSizeRange = [min, max] 31 | this.propHeight = ~~(max / 2) 32 | this.propDistanceRange = [~~(min / 2), max * 2] 33 | 34 | this.stage = null 35 | this.propCreator = null 36 | 37 | this.init() 38 | } 39 | 40 | init () { 41 | this.initStage() 42 | this.initPropCreator() 43 | this.computeSafeClearLength() 44 | // 第一个道具 45 | this.createProp(0) 46 | // 第二个道具 47 | this.createProp(0) 48 | // 首次调整相机 49 | this.moveCamera(0) 50 | this.initLittleMan() 51 | 52 | // 测试 53 | // const autoMove = () => { 54 | // setTimeout(() => { 55 | // autoMove() 56 | // this.createProp() 57 | // this.moveCamera() 58 | // }, 2000) 59 | // } 60 | // autoMove() 61 | } 62 | 63 | // 初始化舞台 64 | initStage () { 65 | const { container, canvas, axesHelper, width, height } = this 66 | const cameraNear = 0.1 67 | const cameraFar = 2000 68 | // 计算相机应该放在哪里 69 | const cameraInitalPosition = this.cameraInitalPosition = computeCameraInitalPosition(35, 225, height / 2, height / 2, cameraNear, cameraFar) 70 | const lightInitalPosition = this.lightInitalPosition = { x: -300, y: 600, z: 200 } 71 | 72 | this.stage = new Stage({ 73 | width, 74 | height, 75 | canvas, 76 | axesHelper, 77 | cameraNear, 78 | cameraFar, 79 | cameraInitalPosition, 80 | lightInitalPosition 81 | }) 82 | } 83 | 84 | // 初始化道具生成器 85 | initPropCreator () { 86 | const { needDefaultCreator, propSizeRange, propHeight } = this 87 | 88 | // 管理所有道具 89 | this.props = [] 90 | this.propCreator = new PropCreator({ 91 | propHeight, 92 | propSizeRange, 93 | needDefaultCreator 94 | }) 95 | } 96 | 97 | // 对外的新增生成器的接口 98 | createPropCreator (...args) { 99 | this.propCreator.createPropCreator(...args) 100 | } 101 | 102 | // 初始化小人 103 | initLittleMan () { 104 | const { stage, propHeight, G, props } = this 105 | const littleMan = this.littleMan = new LittleMan({ 106 | world: this, 107 | color: 0x386899, 108 | G 109 | }) 110 | littleMan.enterStage(stage, { x: 0, y: propHeight + 80, z: 0 }, props[0]) 111 | littleMan.jump() 112 | } 113 | 114 | // 创建盒子 115 | createProp (enterHeight = 100) { 116 | const { 117 | height, 118 | propCreator, 119 | propHeight, 120 | propSizeRange: [min, max], 121 | propDistanceRange, 122 | stage, props, 123 | props: { length } 124 | } = this 125 | const currentProp = props[length - 1] 126 | const prop = new Prop({ 127 | world: this, 128 | stage, 129 | // 头2个盒子用第一个创造器生成 130 | body: propCreator.createProp(length < 3 ? 0 : -1), 131 | height: propHeight, 132 | prev: currentProp, 133 | enterHeight, 134 | distanceRange: propDistanceRange 135 | }) 136 | const size = prop.getSize() 137 | 138 | if (size.y !== propHeight) { 139 | console.warn(`高度: ${size.y},盒子高度必须为 ${propHeight}`) 140 | } 141 | if (size.x < min || size.x > max) { 142 | console.warn(`宽度: ${size.x}, 盒子宽度必须为 ${min} - ${max}`) 143 | } 144 | if (size.z < min || size.z > max) { 145 | console.warn(`深度: ${size.z}, 盒子深度度必须为 ${min} - ${max}`) 146 | } 147 | 148 | if (currentProp) { 149 | currentProp.setNext(prop) 150 | } 151 | 152 | prop.enterStage() 153 | props.push(prop) 154 | } 155 | 156 | // 移动相机,总是看向最后2个小球的中间位置 157 | moveCamera (duration = 500) { 158 | const { 159 | stage, 160 | height, 161 | cameraInitalPosition: { x: cameraX, y: cameraY, z: cameraZ }, 162 | lightInitalPosition: { x: lightX, y: lightY, z: lightZ } 163 | } = this 164 | // 将可视区向上偏移一点,这样看起来道具的位置更合理 165 | const cameraOffsetY = height / 10 166 | 167 | const { x, y, z } = this.getLastTwoCenterPosition() 168 | const cameraTo = { 169 | x: x + cameraX + cameraOffsetY, 170 | y: cameraY, // 高度是不变的 171 | z: z + cameraZ + cameraOffsetY 172 | } 173 | const lightTo = { 174 | x: x + lightX, 175 | y: lightY, 176 | z: z + lightZ 177 | } 178 | 179 | // 移动舞台相机 180 | const options = { 181 | cameraTo, 182 | lightTo, 183 | center: { x, y, z } 184 | } 185 | stage.moveCamera( 186 | options, 187 | () => { 188 | this.clearProps() 189 | }, 190 | duration 191 | ) 192 | } 193 | 194 | // 计算最新的2个盒子的中心点 195 | getLastTwoCenterPosition () { 196 | const { props, props: { length } } = this 197 | const { x: x1, z: z1 } = props[length - 2].getPosition() 198 | const { x: x2, z: z2 } = props[length - 1].getPosition() 199 | 200 | return { 201 | x: x1 + (x2 - x1) / 2, 202 | z: z1 + (z2 - z1) / 2 203 | } 204 | } 205 | 206 | // 销毁道具 207 | clearProps () { 208 | const { 209 | width, 210 | height, 211 | safeClearLength, 212 | props, stage, 213 | props: { length } 214 | } = this 215 | const point = 4 216 | 217 | if (length > safeClearLength) { 218 | props.slice(0, point).forEach(prop => prop.dispose()) 219 | this.props = props.slice(point) 220 | } 221 | } 222 | 223 | // 估算销毁安全值 224 | computeSafeClearLength () { 225 | const { width, height, propSizeRange } = this 226 | const minS = propSizeRange[0] 227 | const hypotenuse = Math.sqrt(minS * minS + minS * minS) 228 | this.safeClearLength = Math.ceil(width / minS) + Math.ceil(height / hypotenuse / 2) + 1 229 | } 230 | } 231 | 232 | export default JumpGameWorld -------------------------------------------------------------------------------- /src/games/wx-jump-2/utils.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import TWEEN from '@tweenjs/tween.js' 3 | 4 | const { random, sqrt, floor, pow, sin, cos, tan, PI } = Math 5 | 6 | export let propCounter = 0 7 | export const incrementPropCounter = () => propCounter++ 8 | export const resetPropCounter = () => (propCounter = 0) 9 | 10 | export const colors = [0x67C23A, 0xE6A23C, 0xF56C6C, 0x909399, 0x409EFF, 0xffffff] 11 | 12 | /** 13 | * 根据角度计算相机初始位置 14 | * @param {Number} verticalDeg 相机和场景中心点的垂直角度 15 | * @param {Number} horizontalDeg 相机和x轴的水平角度 16 | * @param {Number} top 相机上侧面 17 | * @param {Number} bottom 相机下侧面 18 | * @param {Number} near 摄像机视锥体近端面 19 | * @param {Number} far 摄像机视锥体远端面 20 | */ 21 | export function computeCameraInitalPosition (verticalDeg, horizontalDeg, top, bottom, near, far) { 22 | const verticalRadian = verticalDeg * (PI / 180) 23 | const horizontalRadian = horizontalDeg * (PI / 180) 24 | const minY = cos(verticalRadian) * bottom 25 | const maxY = sin(verticalRadian) * (far - near - top / tan(verticalRadian)) 26 | 27 | if (minY > maxY) { 28 | console.warn('警告: 垂直角度太小了!') 29 | return 30 | } 31 | const y = minY + (maxY - minY) / 2 32 | const longEdge = y / tan(verticalRadian) 33 | const x = sin(horizontalRadian) * longEdge 34 | const z = cos(horizontalRadian) * longEdge 35 | 36 | return { x, y, z } 37 | } 38 | 39 | // 材质 40 | export const baseMeshLambertMaterial = new THREE.MeshLambertMaterial() 41 | // 立方体 42 | export const baseBoxBufferGeometry = new THREE.BoxBufferGeometry(1, 1, 1, 10, 4, 10) 43 | baseBoxBufferGeometry.userData.type = 'box' 44 | // 圆柱体 45 | export const baseCylinderBufferGeometry = new THREE.CylinderBufferGeometry(1, 1, 1, 30, 5) 46 | baseCylinderBufferGeometry.userData.type = 'Cylinder' 47 | 48 | // 物体销毁 49 | export const destroyMesh = mesh => { 50 | if (mesh.geometry) { 51 | mesh.geometry.dispose() 52 | mesh.geometry = null 53 | } 54 | if (mesh.material) { 55 | mesh.material.dispose() 56 | mesh.material = null 57 | } 58 | 59 | mesh.parent.remove(mesh) 60 | 61 | mesh.parent = null 62 | mesh = null 63 | } 64 | 65 | export const randomArrayElm = array => array[floor(random() * array.length)] 66 | 67 | export const rangeNumberInclusive = (min, max) => floor(random() * (max - min + 1)) + min 68 | 69 | export const getPropSize = box => { 70 | const box3 = getPropSize.box3 || (getPropSize.box3 = new THREE.Box3()) 71 | box3.setFromObject(box) 72 | return box3.getSize(new THREE.Vector3()) 73 | } 74 | 75 | // 斜抛计算 76 | export const computeObligueThrowValue = function (v0, theta, G) { 77 | const sin2θ = sin(2 * theta) 78 | const sinθ = sin(theta) 79 | 80 | const rangeR = pow(v0, 2) * sin2θ / G 81 | const rangeH = pow(v0 * sinθ, 2) / (2 * G) 82 | // const rangeT = 2 * v0 * sinθ / G 83 | 84 | return { 85 | rangeR, 86 | rangeH, 87 | // rangeT 88 | } 89 | } 90 | /** 91 | * 获取静止盒子的碰撞检测器 92 | * @param {Mesh} prop 检测的盒子 93 | * @param {String} direction 物体过来的方向(世界坐标系) 94 | * @param {Boolean} isForward 基于方向的前后 95 | */ 96 | export const getHitValidator = (prop, direction, isForward) => { 97 | const origin = prop.position.clone() 98 | const vertices = prop.geometry.attributes.position 99 | const length = vertices.count 100 | 101 | // 盒子是静止的,先将顶点到中心点的向量准备好,避免重复计算 102 | const directionVectors = Array.from({ length }) 103 | .map((_, i) => new THREE.Vector3().fromBufferAttribute(vertices, i)) 104 | .filter(vector3 => { 105 | // 过滤掉一部分盒子离小人远端的顶点 106 | if (direction === 'z' && isForward) { 107 | // 从当前盒子倒向目标盒子 108 | return vector3.z < 0 109 | } else if (direction === 'z') { 110 | // 从目标盒子倒向当前盒子 111 | return vector3.z > 0 112 | } else if (direction === 'x' && isForward) { 113 | return vector3.x < 0 114 | } else if (direction === 'x') { 115 | return vector3.x > 0 116 | } 117 | }) 118 | .map(localVertex => { 119 | const globaVertex = localVertex.applyMatrix4(prop.matrix) 120 | // 先将向量准备好 121 | return globaVertex.sub(prop.position) 122 | }) 123 | 124 | return littleMan => { 125 | for (let i = 0, directionVector; (directionVector = directionVectors[i]); i++) { 126 | const raycaster = new THREE.Raycaster(origin, directionVector.clone().normalize()) 127 | const collisionResults = raycaster.intersectObject(littleMan, true) 128 | 129 | // 发生了碰撞 130 | if(collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() + 1.2 ){ 131 | return true 132 | } 133 | } 134 | return false 135 | } 136 | } 137 | 138 | export const showSegments = mesh => { 139 | const geometry = mesh.geometry.clone() 140 | const material = new THREE.PointsMaterial({ 141 | size: 3, 142 | color: 0x58D68D, 143 | transparent: true 144 | }) 145 | const particle = new THREE.Points(geometry, material) 146 | 147 | mesh.add(particle) 148 | } 149 | 150 | /** 151 | * 根据射程算出落地点 152 | * @param {Number} range 射程 153 | * @param {Object} c1 起跳点 154 | * @param {Object} p2 目标盒子中心点 155 | */ 156 | export const computePositionByRangeR = function (range, c1, p2) { 157 | const { x: c1x, z: c1z } = c1 158 | const { x: p2x, z: p2z } = p2 159 | 160 | const p2cx = p2x - c1x 161 | const p2cz = p2z - c1z 162 | const p2c = sqrt(pow(p2cz, 2) + pow(p2cx, 2)) 163 | 164 | const jumpDownX = p2cx * range / p2c 165 | const jumpDownZ = p2cz * range / p2c 166 | 167 | return { 168 | jumpDownX: c1x + jumpDownX, 169 | jumpDownZ: c1z + jumpDownZ 170 | } 171 | } 172 | 173 | export const animate = (configs, onUpdate, onComplete) => { 174 | const { 175 | from, to, duration, 176 | easing = k => k, 177 | autoStart = true // 为了使用tween的chain 178 | } = configs 179 | 180 | const tween = new TWEEN.Tween(from) 181 | .to(to, duration) 182 | .easing(easing) 183 | .onUpdate(onUpdate) 184 | .onComplete(() => { 185 | onComplete && onComplete() 186 | }) 187 | 188 | if (autoStart) { 189 | tween.start() 190 | } 191 | 192 | animateFrame() 193 | return tween 194 | } 195 | 196 | const animateFrame = function () { 197 | if (animateFrame.openin) { 198 | return 199 | } 200 | animateFrame.openin = true 201 | 202 | const animate = () => { 203 | const id = requestAnimationFrame(animate) 204 | if (!TWEEN.update()) { 205 | animateFrame.openin = false 206 | cancelAnimationFrame(id) 207 | } 208 | } 209 | animate() 210 | } -------------------------------------------------------------------------------- /src/games/wx-jump-2/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | computeCameraInitalPosition, 3 | resetPropCounter 4 | } from './utils' 5 | import Stage from './Stage' 6 | import PropCreator from './PropCreator' 7 | import Prop from './Prop' 8 | import LittleMan from './LittleMan' 9 | 10 | class JumpGameWorld { 11 | constructor ({ 12 | container, 13 | canvas, 14 | needDefaultCreator = true, 15 | axesHelper = false 16 | }) { 17 | const { offsetWidth, offsetHeight } = container 18 | this.G = 9.8 19 | this.container = container 20 | this.canvas = canvas 21 | this.width = offsetWidth 22 | this.height = offsetHeight 23 | this.needDefaultCreator = needDefaultCreator 24 | this.axesHelper = axesHelper 25 | 26 | const [min, max] = [~~(offsetWidth / 6), ~~(offsetWidth / 3.5)] 27 | this.propSizeRange = [min, max] 28 | this.propHeight = ~~(max / 2) 29 | this.propDistanceRange = [~~(min / 2), max * 2] 30 | 31 | this.stage = null 32 | this.propCreator = null 33 | this.littleMans = [] 34 | this.props = [] 35 | 36 | this.init() 37 | } 38 | 39 | init () { 40 | this.initStage() 41 | this.initPropCreator() 42 | this.computeSafeClearLength() 43 | // 第一个道具 44 | this.createProp(0) 45 | // 第二个道具 46 | this.createProp(0) 47 | // 首次调整相机 48 | this.moveCamera(0) 49 | this.initLittleMan() 50 | 51 | // 测试 52 | // const autoMove = () => { 53 | // setTimeout(() => { 54 | // autoMove() 55 | // this.createProp() 56 | // this.moveCamera() 57 | // }, 2000) 58 | // } 59 | // autoMove() 60 | } 61 | 62 | // 游戏销毁 63 | destroy () { 64 | this.clear() 65 | } 66 | 67 | // 让游戏回到初始状态 68 | clear () { 69 | this.stage && this.stage.reset() 70 | 71 | this.props.forEach(prop => prop.destroy()) 72 | this.props = [] 73 | resetPropCounter() 74 | 75 | this.littleMans.forEach(littleMan => littleMan.destroy()) 76 | this.littleMans = [] 77 | } 78 | 79 | // 初始化舞台 80 | initStage () { 81 | const { canvas, axesHelper, width, height } = this 82 | const cameraNear = 0.1 83 | const cameraFar = 2000 84 | // 计算相机应该放在哪里 85 | const cameraInitalPosition = this.cameraInitalPosition = computeCameraInitalPosition(35, 225, height / 2, height / 2, cameraNear, cameraFar) 86 | const lightInitalPosition = this.lightInitalPosition = { x: -300, y: 600, z: 200 } 87 | 88 | this.stage = new Stage({ 89 | width, 90 | height, 91 | canvas, 92 | axesHelper, 93 | cameraNear, 94 | cameraFar, 95 | cameraInitalPosition, 96 | lightInitalPosition 97 | }) 98 | } 99 | 100 | // 初始化道具生成器 101 | initPropCreator () { 102 | const { needDefaultCreator, propSizeRange, propHeight } = this 103 | 104 | // 管理所有道具 105 | this.propCreator = new PropCreator({ 106 | propHeight, 107 | propSizeRange, 108 | needDefaultCreator 109 | }) 110 | } 111 | 112 | // 对外的新增生成器的接口 113 | createPropCreator (...args) { 114 | this.propCreator.createPropCreator(...args) 115 | } 116 | 117 | // 初始化小人 118 | initLittleMan () { 119 | const { stage, propHeight, G, props } = this 120 | const littleMan = new LittleMan({ 121 | world: this, 122 | color: 0x386899, 123 | G 124 | }) 125 | littleMan.enterStage(stage, { x: 0, y: propHeight + 80, z: 0 }, props[0]) 126 | littleMan.jump() 127 | 128 | this.littleMans.push(littleMan) 129 | } 130 | 131 | // 创建盒子 132 | createProp (enterHeight = 100, index = -1) { 133 | const { 134 | propCreator, 135 | propHeight, 136 | propSizeRange: [min, max], 137 | propDistanceRange, 138 | stage, props, 139 | props: { length } 140 | } = this 141 | const currentProp = props[length - 1] 142 | const prop = new Prop({ 143 | world: this, 144 | stage, 145 | // 头2个盒子用第一个创造器生成 146 | body: propCreator.createProp(index === -1 ? length < 3 ? 0 : index : index), 147 | height: propHeight, 148 | prev: currentProp, 149 | enterHeight, 150 | distanceRange: propDistanceRange 151 | }) 152 | const size = prop.getSize() 153 | 154 | if (size.y !== propHeight) { 155 | console.warn(`高度: ${size.y},盒子高度必须为 ${propHeight}`) 156 | } 157 | if (size.x < min || size.x > max) { 158 | console.warn(`宽度: ${size.x}, 盒子宽度必须为 ${min} - ${max}`) 159 | } 160 | if (size.z < min || size.z > max) { 161 | console.warn(`深度: ${size.z}, 盒子深度度必须为 ${min} - ${max}`) 162 | } 163 | 164 | if (currentProp) { 165 | currentProp.setNext(prop) 166 | } 167 | 168 | prop.enterStage() 169 | props.push(prop) 170 | 171 | return prop 172 | } 173 | 174 | // 移动相机,总是看向最后2个盒子的中间位置 175 | moveCamera (duration = 500) { 176 | const { 177 | stage, 178 | height, 179 | cameraInitalPosition: { x: cameraX, y: cameraY, z: cameraZ }, 180 | lightInitalPosition: { x: lightX, y: lightY, z: lightZ } 181 | } = this 182 | // 将可视区向上偏移一点,这样看起来道具的位置更合理 183 | const cameraOffsetY = height / 10 184 | 185 | const { x, y, z } = this.getLastTwoCenterPosition() 186 | const cameraTo = { 187 | x: x + cameraX + cameraOffsetY, 188 | y: cameraY, // 高度是不变的 189 | z: z + cameraZ + cameraOffsetY 190 | } 191 | const lightTo = { 192 | x: x + lightX, 193 | y: lightY, 194 | z: z + lightZ 195 | } 196 | 197 | // 移动舞台相机 198 | const options = { 199 | cameraTo, 200 | lightTo, 201 | center: { x, y, z } 202 | } 203 | stage.moveCamera( 204 | options, 205 | () => { 206 | this.clearProps() 207 | }, 208 | duration 209 | ) 210 | } 211 | 212 | // 计算最新的2个盒子的中心点 213 | getLastTwoCenterPosition () { 214 | const { props, props: { length } } = this 215 | const { x: x1, z: z1 } = props[length - 2].getPosition() 216 | const { x: x2, z: z2 } = props[length - 1].getPosition() 217 | 218 | return { 219 | x: x1 + (x2 - x1) / 2, 220 | z: z1 + (z2 - z1) / 2 221 | } 222 | } 223 | 224 | // 销毁道具 225 | clearProps () { 226 | const { 227 | safeClearLength, 228 | props, 229 | props: { length } 230 | } = this 231 | const point = 4 232 | 233 | if (length > safeClearLength) { 234 | props.slice(0, point).forEach(prop => prop.destroy()) 235 | this.props = props.slice(point) 236 | } 237 | } 238 | 239 | // 估算销毁安全值 240 | computeSafeClearLength () { 241 | const { width, height, propSizeRange } = this 242 | const minS = propSizeRange[0] 243 | const hypotenuse = Math.sqrt(minS * minS + minS * minS) 244 | this.safeClearLength = Math.ceil(width / minS) + Math.ceil(height / hypotenuse / 2) + 1 245 | } 246 | } 247 | 248 | export default JumpGameWorld -------------------------------------------------------------------------------- /examples/js/app.f1e8862e.js: -------------------------------------------------------------------------------- 1 | (function(e){function t(t){for(var r,a,s=t[0],i=t[1],c=t[2],p=0,l=[];p { 110 | body.position.setY(y) 111 | stage.render() 112 | } 113 | ) 114 | } 115 | 116 | // 回弹动画 117 | springbackTransition (duration) { 118 | const { body, stage } = this 119 | const y = body.scale.y 120 | 121 | animate( 122 | { 123 | from: { y }, 124 | to: { y: 1 }, 125 | duration, 126 | easing: TWEEN.Easing.Bounce.Out 127 | }, 128 | ({ y }) => { 129 | body.scale.setY(y) 130 | stage.render() 131 | } 132 | ) 133 | } 134 | 135 | // containsPoint (x, z) { 136 | // const { body } = this 137 | // const { type } = body.geometry.userData 138 | // const { x: sx, z: sz } = this.getSize() 139 | // const { x: px, z: pz } = this.getPosition() 140 | 141 | // if (type === 'box') { 142 | // const halfSx = sx / 2 143 | // const halfSz = sz / 2 144 | // const minX = px - halfSx 145 | // const maxX = px + halfSx 146 | // const minZ = pz - halfSz 147 | // const maxZ = pz + halfSz 148 | 149 | // return x >= minX && x <= maxX && z >= minZ && z <= maxZ 150 | // } else { 151 | // const radius = sx / 2 152 | // // 小人脚下中心点离圆心的距离 153 | // const distance = Math.sqrt(Math.pow(px - x, 2) + Math.pow(pz - z, 2)) 154 | 155 | // return distance <= radius 156 | // } 157 | // } 158 | 159 | /** 160 | * 计算跌落数据 161 | * @param {Number} width 小人的宽度 162 | * @param {Number} x 小人脚下中心点的X值 163 | * @param {Number} z 小人脚下中心点的Z值 164 | * @return { 165 | * contains, // 小人中心点是否在盒子上 166 | * isEdge, // 是否在边缘 167 | * translateZ, // 将小人旋转部分移动 -translateZ,将网格移动translateZ 168 | * degY, // 调整小人方向,然后使用小人的本地坐标进行平移和旋转 169 | * } 170 | */ 171 | computePointInfos (width, x, z) { 172 | const { body } = this 173 | 174 | if (!body) { 175 | return {} 176 | } 177 | 178 | const { type } = body.geometry.userData 179 | const { x: sx, z: sz } = this.getSize() 180 | const { x: px, z: pz } = this.getPosition() 181 | const halfWidth = width / 2 182 | 183 | // 立方体和圆柱体的计算逻辑略有差别 184 | if (type === 'box') { 185 | const halfSx = sx / 2 186 | const halfSz = sz / 2 187 | const minX = px - halfSx 188 | const maxX = px + halfSx 189 | const minZ = pz - halfSz 190 | const maxZ = pz + halfSz 191 | 192 | const contains = x >= minX && x <= maxX && z >= minZ && z <= maxZ 193 | 194 | if (contains) { 195 | return { contains } 196 | } 197 | 198 | const translateZ1 = Math.abs(z - pz) - halfSz 199 | const translateZ2 = Math.abs(x - px) - halfSx 200 | // 半空中 201 | if (translateZ1 >= halfWidth || translateZ2 >= halfWidth) { 202 | return { contains } 203 | } 204 | 205 | // 计算是否在盒子的边缘 206 | let isEdge = false 207 | let degY = 0 208 | let translateZ = 0 209 | 210 | // 四个方向上都有可能 211 | if (x < maxX && x > minX) { 212 | if (z > maxZ && z < maxZ + halfWidth) { 213 | degY = 0 214 | } else if (z < minZ && z > minZ - halfWidth) { 215 | degY = 180 216 | } 217 | isEdge = true 218 | translateZ = translateZ1 219 | } else if (z < maxZ && z > minZ) { 220 | if (x > maxX && x < maxX + halfWidth) { 221 | degY = 90 222 | } else if (x < minX && x > minX - halfWidth) { 223 | degY = 270 224 | } 225 | isEdge = true 226 | translateZ = translateZ2 227 | } 228 | 229 | return { 230 | contains, 231 | translateZ, 232 | isEdge, 233 | degY 234 | } 235 | } else { 236 | const radius = sx / 2 237 | // 小人脚下中心点离圆心的距离 238 | const distance = Math.sqrt(Math.pow(px - x, 2) + Math.pow(pz - z, 2)) 239 | 240 | const contains = distance <= radius 241 | 242 | if (contains) { 243 | return { contains } 244 | } 245 | 246 | // 半空中 247 | if (distance >= radius + halfWidth) { 248 | return { contains } 249 | } 250 | 251 | // 在圆柱体的边缘 252 | const isEdge = true 253 | const translateZ = distance - radius 254 | 255 | let degY = Math.atan(Math.abs(x - px) / Math.abs(z - pz)) * 180 / Math.PI 256 | 257 | if (x === px) { 258 | degY = z > pz ? 0 : 180 259 | } else if (z === pz) { 260 | degY = x > px ? 90 : 270 261 | } else if (x > px && z > pz) { 262 | } else if (x > px && z < pz) { 263 | degY = 180 - degY 264 | } else if (z < pz) { 265 | degY = 180 + degY 266 | } else { 267 | degY = 360 - degY 268 | } 269 | 270 | return { 271 | contains, 272 | translateZ, 273 | isEdge, 274 | degY 275 | } 276 | } 277 | } 278 | 279 | setNext (next) { 280 | this.next = next 281 | } 282 | 283 | getNext () { 284 | return this.next 285 | } 286 | 287 | getPosition () { 288 | return this.body.position 289 | } 290 | 291 | setPosition (x, y, z) { 292 | return this.body.position.set(x, y, z) 293 | } 294 | 295 | scaleY (y) { 296 | return this.body.scale.setY(y) 297 | } 298 | 299 | // 获取道具大小 300 | getSize () { 301 | return getPropSize(this.body) 302 | } 303 | 304 | // 销毁道具 305 | destroy () { 306 | if (this.prev) { 307 | this.prev.next = null 308 | } 309 | if (this.next) { 310 | this.next.prev = null 311 | } 312 | 313 | destroyMesh(this.body) 314 | 315 | this.world = null 316 | this.stage = null 317 | this.body = null 318 | this.prev = null 319 | this.next = null 320 | } 321 | } 322 | 323 | export default Prop -------------------------------------------------------------------------------- /src/games/wx-jump-2/Particle.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { getPropSize, rangeNumberInclusive, destroyMesh } from './utils' 3 | 4 | class Particle { 5 | constructor ({ 6 | world, 7 | quantity = 15, // 数量 8 | triggerObject // 触发对象 9 | }) { 10 | this.world = world 11 | this.quantity = quantity 12 | this.triggerObject = triggerObject 13 | this.particleSystem = null 14 | 15 | const { x, y } = getPropSize(triggerObject) 16 | 17 | this.triggerObjectWidth = x 18 | 19 | // 粒子流,垂直方向的范围,约定从小人的上半身出现,算上粒子最大大小 20 | const flowSizeRange = this.flowSizeRange = [x / 6, x / 3] 21 | this.flowRangeY = [y / 2, y - flowSizeRange[1]] 22 | // 粒子初始的y值应该是粒子大小的最大值 23 | this.initalY = flowSizeRange[1] 24 | // 限制粒子水平方向的范围 25 | this.flowRangeX = [-x * 2, x * 2] 26 | 27 | // 粒子喷泉,垂直方向的范围,约定从小人的下半身出现,算上粒子最大大小 28 | const fountainSizeRange = this.fountainSizeRange = this.flowSizeRange.map(s => s / 2) 29 | this.fountainRangeY = [fountainSizeRange[1], y / 3] 30 | this.fountainRangeDistance = [y / 4, y / 2] 31 | // 限制粒子水平方向的范围 32 | this.fountainRangeX = [-x / 3, x / 3] 33 | 34 | this.createParticle() 35 | } 36 | 37 | // 销毁粒子 38 | destroy () { 39 | if (this.particleSystem) { 40 | this.particleSystem.children.forEach(destroyMesh) 41 | this.particleSystem.children = null 42 | destroyMesh(this.particleSystem) 43 | } 44 | 45 | this.world = null 46 | this.triggerObject = null 47 | this.particleSystem = null 48 | } 49 | 50 | // 生成粒子 51 | createParticle () { 52 | const { quantity, triggerObject } = this 53 | // 一半白色、一半绿色 54 | const white = new THREE.Color( 0xffffff ) 55 | const green = new THREE.Color( 0x58D68D ) 56 | const colors = Array.from({ length: quantity }).map((_, i) => i % 2 ? white : green) 57 | const particleSystem = this.particleSystem = new THREE.Group() 58 | 59 | new THREE.TextureLoader().load(require('./dot.png'), dot => { 60 | const baseGeometry = new THREE.Geometry() 61 | baseGeometry.vertices.push(new THREE.Vector3()) 62 | 63 | const baseMaterial = new THREE.PointsMaterial({ 64 | size: 0, 65 | map: dot, 66 | // depthTest: false, // 开启后可以透视... 67 | transparent: true 68 | }) 69 | 70 | colors.forEach(color => { 71 | const geometry = baseGeometry.clone() 72 | const material = baseMaterial.clone() 73 | material.setValues({ color }) 74 | 75 | const particle = new THREE.Points(geometry, material) 76 | particleSystem.add(particle) 77 | }) 78 | 79 | this.resetParticle() 80 | 81 | triggerObject.add(particleSystem) 82 | }) 83 | } 84 | 85 | // 将粒子放到小人脚下 86 | resetParticle () { 87 | const { particleSystem, initalY } = this 88 | particleSystem.children.forEach(particle => { 89 | particle.position.y = initalY 90 | particle.position.x = 0 91 | particle.position.z = 0 92 | }) 93 | } 94 | 95 | // 粒子流粒子泵 96 | runParticleFlowPump () { 97 | const { particleSystem, quantity, initalY } = this 98 | // 粒子泵只关心脚下的粒子(水池) 99 | const particles = particleSystem.children.filter(child => child.position.y === initalY) 100 | 101 | // 脚下的粒子量不够,抽不上来 102 | if (particles.length < quantity / 3) { 103 | return 104 | } 105 | 106 | const { 107 | triggerObjectWidth, 108 | flowRangeX, flowRangeY, flowSizeRange 109 | } = this 110 | // 比如随机 x 值为0,这个值在小人的身体范围内,累加一个1/2身体宽度,这样做可能有部分区域随机不到,不过影响不大 111 | const halfWidth = triggerObjectWidth / 2 112 | 113 | particles.forEach(particle => { 114 | const { position, material } = particle 115 | const randomX = rangeNumberInclusive(...flowRangeX) 116 | const randomZ = rangeNumberInclusive(...flowRangeX) 117 | // 小人的身体内,不能成为起点,需要根据正反将身体的宽度加上 118 | const excludeX = randomX < 0 ? -halfWidth : halfWidth 119 | const excludeZ = randomZ < 0 ? -halfWidth : halfWidth 120 | 121 | position.x = excludeX + randomX 122 | position.z = excludeZ + randomZ 123 | position.y = rangeNumberInclusive(...flowRangeY) 124 | 125 | material.setValues({ size: rangeNumberInclusive(...flowSizeRange) }) 126 | }) 127 | } 128 | 129 | // 粒子流 130 | runParticleFlow () { 131 | if (this.runingParticleFlow) { 132 | return 133 | } 134 | this.runingParticleFlow = true 135 | 136 | const { world, triggerObjectWidth, particleSystem, initalY } = this 137 | let prevTime = 0 138 | // 约定速度,每毫秒走多远 139 | const speed = triggerObjectWidth * 3 / 1000 140 | const animate = () => { 141 | const id = requestAnimationFrame(animate) 142 | 143 | if (this.runingParticleFlow) { 144 | // 抽粒子 145 | this.runParticleFlowPump() 146 | if (prevTime) { 147 | const actives = particleSystem.children.filter(child => child.position.y !== initalY) 148 | const diffTime = Date.now() - prevTime 149 | // 粒子的行程 150 | const trip = diffTime * speed 151 | 152 | actives.forEach(particle => { 153 | const { position } = particle 154 | const { x, y, z } = position 155 | 156 | if (y < initalY) { 157 | // 只要粒子的y值超过安全值,就认为它已经到达终点 158 | position.y = initalY 159 | position.x = 0 160 | position.z = 0 161 | } else { 162 | const distance = Math.sqrt(Math.pow(x, 2) + Math.pow(z, 2) + Math.pow(y - initalY, 2)) 163 | const ratio = (distance - trip) / distance 164 | 165 | position.x = ratio * x 166 | position.z = ratio * z 167 | position.y = ratio * y 168 | } 169 | }) 170 | world.stage.render() 171 | } 172 | prevTime = Date.now() 173 | } else { 174 | cancelAnimationFrame(id) 175 | } 176 | } 177 | animate() 178 | } 179 | 180 | // 停止粒子流 181 | stopRunParticleFlow () { 182 | this.runingParticleFlow = false 183 | this.resetParticle() 184 | } 185 | 186 | // 粒子喷泉粒子泵 187 | runParticleFountainPump (particles, duration) { 188 | const { fountainRangeDistance, triggerObjectWidth, initalY, world } = this 189 | // 随机设置粒子的终点 190 | particles.forEach(particle => { 191 | const { position: { x, y, z } } = particle 192 | 193 | const userData = particle.userData 194 | 195 | userData.ty = y + rangeNumberInclusive(...fountainRangeDistance) 196 | // x轴和z轴 向外侧喷出 197 | const diffX = rangeNumberInclusive(0, triggerObjectWidth / 3) 198 | userData.tx = (x < 0 ? -diffX : diffX) + x 199 | const diffZ = rangeNumberInclusive(0, triggerObjectWidth / 3) 200 | userData.tz = (z < 0 ? -diffZ : diffZ) + z 201 | }) 202 | 203 | let prevTime = 0 204 | const startTime = Date.now() 205 | const speed = triggerObjectWidth * 3 / 600 206 | 207 | const animate = () => { 208 | const id = requestAnimationFrame(animate) 209 | // 已经在脚下的粒子不用处理 210 | const actives = particles.filter(particle => particle.position.y !== initalY) 211 | 212 | if (actives.length && !this.runingParticleFlow && Date.now() - startTime < duration) { 213 | if (prevTime) { 214 | const diffTime = Date.now() - prevTime 215 | // 粒子的行程 216 | const trip = diffTime * speed 217 | 218 | actives.forEach(particle => { 219 | const { 220 | position, 221 | position: { x, y, z }, 222 | userData: { tx, ty, tz } 223 | } = particle 224 | if (y >= ty) { 225 | // 已经到达终点的粒子,重新放到脚下去 226 | position.x = 0 227 | position.y = initalY 228 | position.z = 0 229 | // 清空ratio值 230 | } else { 231 | const diffX = tx - x 232 | const diffY = ty - y 233 | const diffZ = tz - z 234 | const distance = Math.sqrt(Math.pow(diffX, 2) + Math.pow(diffY, 2) + Math.pow(diffZ, 2)) 235 | const ratio = trip / distance 236 | 237 | position.y += ratio * diffY 238 | position.x += ratio * diffX 239 | position.z += ratio * diffZ 240 | } 241 | }) 242 | world.stage.render() 243 | } 244 | prevTime = Date.now() 245 | } else { 246 | this.runingParticleFountain = false 247 | cancelAnimationFrame(id) 248 | } 249 | } 250 | animate() 251 | } 252 | 253 | // 粒子喷泉 254 | runParticleFountain () { 255 | if (this.runingParticleFountain) { 256 | return 257 | } 258 | this.runingParticleFountain = true 259 | 260 | const { particleSystem, quantity, initalY } = this 261 | // 粒子泵只关心脚下的粒子(水池) 262 | const particles = particleSystem.children.filter(child => child.position.y === initalY).slice(0, quantity) 263 | 264 | if (!particles.length) { 265 | return 266 | } 267 | 268 | const { 269 | triggerObjectWidth, 270 | fountainRangeX, fountainSizeRange, fountainRangeY 271 | } = this 272 | const halfWidth = triggerObjectWidth / 2 273 | 274 | particles.forEach(particle => { 275 | const { position, material } = particle 276 | const randomX = rangeNumberInclusive(...fountainRangeX) 277 | const randomZ = rangeNumberInclusive(...fountainRangeX) 278 | // 小人的身体内,不能成为起点,需要根据正反将身体的宽度加上 279 | const excludeX = randomX < 0 ? -halfWidth : halfWidth 280 | const excludeZ = randomZ < 0 ? -halfWidth : halfWidth 281 | 282 | position.x = excludeX + randomX 283 | position.z = excludeZ + randomZ 284 | position.y = rangeNumberInclusive(...fountainRangeY) 285 | 286 | material.setValues({ size: rangeNumberInclusive(...fountainSizeRange) }) 287 | }) 288 | 289 | // 喷射粒子 290 | this.runParticleFountainPump(particles, 1000) 291 | } 292 | } 293 | 294 | export default Particle -------------------------------------------------------------------------------- /src/games/wx-jump-1/LittleMan.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import TWEEN from '@tweenjs/tween.js' 3 | import { 4 | baseMeshLambertMaterial, 5 | computeObligueThrowValue, 6 | computePositionByRangeR, 7 | animate 8 | } from './utils' 9 | 10 | class LittleMan { 11 | constructor ({ 12 | world, 13 | color, 14 | G 15 | }) { 16 | this.world = world 17 | this.color = color 18 | this.G = G 19 | this.v0 = world.width / 10 20 | this.theta = 90 21 | 22 | this.headSegment = null 23 | this.bodyScaleSegment = null 24 | this.bodyRotateSegment = null 25 | this.body = null 26 | 27 | this.currentProp = null 28 | this.nextProp = null 29 | this.powerStorageDuration = 1500 30 | 31 | this.stage = null 32 | 33 | this.createBody() 34 | this.resetPowerStorageParameter() 35 | } 36 | 37 | bindEvent () { 38 | const { container } = this.world 39 | const isMobile = 'ontouchstart' in document 40 | const mousedownName = isMobile ? 'touchstart' : 'mousedown' 41 | const mouseupName = isMobile ? 'touchend' : 'mouseup' 42 | 43 | // 该起跳了 44 | const mouseup = () => { 45 | if (this.jumping) { 46 | return 47 | } 48 | this.jumping = true 49 | // 蓄力动作应该停止 50 | this.poweringUp = false 51 | 52 | this.jump() 53 | container.removeEventListener(mouseupName, mouseup) 54 | } 55 | 56 | // 蓄力的时候 57 | const mousedown = event => { 58 | event.preventDefault() 59 | // 跳跃没有完成不能操作 60 | if (this.poweringUp || this.jumping || !this.currentProp) { 61 | return 62 | } 63 | this.poweringUp = true 64 | 65 | this.powerStorage() 66 | container.addEventListener(mouseupName, mouseup, false) 67 | } 68 | 69 | container.addEventListener(mousedownName, mousedown, false) 70 | } 71 | 72 | // 创建身体 73 | createBody () { 74 | const { color, world: { width } } = this 75 | const material = baseMeshLambertMaterial.clone() 76 | material.setValues({ color }) 77 | 78 | // 头部 79 | const headSize = this.headSize = width * .03 80 | const headTranslateY = this.headTranslateY = headSize * 4.5 81 | const headGeometry = new THREE.SphereGeometry(headSize, 40, 40) 82 | const headSegment = this.headSegment = new THREE.Mesh(headGeometry, material) 83 | headSegment.castShadow = true 84 | headSegment.translateY(headTranslateY) 85 | 86 | // 身体 87 | this.bodySize = headSize * 4.5 88 | const bodyBottomGeometry = new THREE.CylinderBufferGeometry(headSize * .9, headSize * 1.2, headSize * 2.5, 40) 89 | bodyBottomGeometry.translate(0, headSize * 1.25, 0) 90 | const bodyCenterGeometry = new THREE.CylinderBufferGeometry(headSize, headSize * .9, headSize, 40) 91 | bodyCenterGeometry.translate(0, headSize * 3, 0) 92 | const bodyTopGeometry = new THREE.SphereGeometry(headSize, 40, 40) 93 | bodyTopGeometry.translate(0, headSize * 3.5, 0) 94 | 95 | const bodyGeometry = new THREE.Geometry() 96 | bodyGeometry.merge(bodyTopGeometry) 97 | bodyGeometry.merge(new THREE.Geometry().fromBufferGeometry(bodyCenterGeometry)) 98 | bodyGeometry.merge(new THREE.Geometry().fromBufferGeometry(bodyBottomGeometry)) 99 | 100 | // 缩放控制 101 | const bodyScaleSegment = this.bodyScaleSegment = new THREE.Mesh(bodyGeometry, material) 102 | bodyScaleSegment.castShadow = true 103 | bodyScaleSegment.translateY(-20) 104 | 105 | // 旋转控制 106 | const bodyRotateSegment = this.bodyRotateSegment = new THREE.Group() 107 | bodyRotateSegment.add(headSegment) 108 | bodyRotateSegment.add(bodyScaleSegment) 109 | bodyRotateSegment.translateY(20) 110 | 111 | // 整体身高 = 头部位移 + 头部高度 / 2 = headSize * 5.5 112 | const body = this.body = new THREE.Group() 113 | body.add(bodyRotateSegment) 114 | } 115 | 116 | // 进入舞台 117 | enterStage (stage, { x, y, z }, nextProp) { 118 | const { body } = this 119 | 120 | body.position.set(x, y, z) 121 | 122 | this.stage = stage 123 | // 进入舞台时告诉小人目标 124 | this.nextProp = nextProp 125 | 126 | stage.add(body) 127 | stage.render() 128 | this.bindEvent() 129 | } 130 | 131 | resetPowerStorageParameter () { 132 | // 由于蓄力导致的变形,需要记录后,在空中将小人复原 133 | this.toValues = { 134 | headTranslateY: 0, 135 | bodyScaleXZ: 0, 136 | bodyScaleY: 0 137 | } 138 | this.fromValues = this.fromValues || { 139 | headTranslateY: this.headTranslateY, 140 | bodyScaleXZ: 1, 141 | bodyScaleY: 1, 142 | propScaleY: 1 143 | } 144 | } 145 | 146 | // 蓄力 147 | powerStorage () { 148 | const { 149 | stage, powerStorageDuration, 150 | body, bodyScaleSegment, headSegment, 151 | bodySize, 152 | fromValues, 153 | currentProp, 154 | world: { propHeight } 155 | } = this 156 | 157 | this.powerStorageTime = Date.now() 158 | this.resetPowerStorageParameter() 159 | 160 | const tween = animate( 161 | { 162 | from: { ...fromValues }, 163 | to: { 164 | headTranslateY: bodySize - bodySize * .6, 165 | bodyScaleXZ: 1.3, 166 | bodyScaleY: .6, 167 | propScaleY: .6 168 | }, 169 | duration: powerStorageDuration 170 | }, 171 | ({ headTranslateY, bodyScaleXZ, bodyScaleY, propScaleY }) => { 172 | if (!this.poweringUp) { 173 | // 抬起时停止蓄力 174 | tween.stop() 175 | } else { 176 | 177 | headSegment.position.setY(headTranslateY) 178 | bodyScaleSegment.scale.set(bodyScaleXZ, bodyScaleY, bodyScaleXZ) 179 | currentProp.scaleY(propScaleY) 180 | body.position.setY(propHeight * propScaleY) 181 | 182 | // 保存此时的位置用于复原 183 | this.toValues = { 184 | headTranslateY, 185 | bodyScaleXZ, 186 | bodyScaleY 187 | } 188 | 189 | stage.render() 190 | } 191 | } 192 | ) 193 | } 194 | 195 | computePowerStorageValue () { 196 | const { powerStorageDuration, powerStorageTime, v0, theta } = this 197 | const diffTime = Date.now() - powerStorageTime 198 | const time = Math.min(diffTime, powerStorageDuration) 199 | const percentage = time / powerStorageDuration 200 | 201 | return { 202 | v0: v0 + 30 * percentage, 203 | theta: theta - 40 * percentage 204 | } 205 | } 206 | 207 | // 跳跃 208 | jump () { 209 | const { 210 | stage, body, 211 | currentProp, nextProp, 212 | world: { propHeight } 213 | } = this 214 | const duration = 400 215 | const start = body.position 216 | const target = nextProp.getPosition() 217 | const { x: startX, y: startY, z: startZ } = start 218 | 219 | // 开始游戏时,小人从第一个盒子正上方入场做弹球下落 220 | if (!currentProp && startX === target.x && startZ === target.z) { 221 | animate( 222 | { 223 | from: { y: startY }, 224 | to: { y: propHeight }, 225 | duration, 226 | easing: TWEEN.Easing.Bounce.Out 227 | }, 228 | ({ y }) => { 229 | body.position.setY(y) 230 | stage.render() 231 | }, 232 | () => { 233 | this.currentProp = nextProp 234 | this.nextProp = nextProp.getNext() 235 | this.jumping = false 236 | } 237 | ) 238 | } else { 239 | if (!currentProp) { 240 | return 241 | } 242 | 243 | const { bodyScaleSegment, headSegment, G, headTranslateY } = this 244 | const { v0, theta } = this.computePowerStorageValue() 245 | const { rangeR, rangeH } = computeObligueThrowValue(v0, theta * (Math.PI / 180), G) 246 | 247 | // 水平匀速 248 | const { jumpDownX, jumpDownZ } = computePositionByRangeR(rangeR, start, target) 249 | animate( 250 | { 251 | from: { 252 | x: startX, 253 | z: startZ, 254 | ...this.toValues 255 | }, 256 | to: { 257 | x: jumpDownX, 258 | z: jumpDownZ, 259 | ...this.fromValues 260 | }, 261 | duration 262 | }, 263 | ({ x, z, headTranslateY, bodyScaleXZ, bodyScaleY }) => { 264 | body.position.setX(x) 265 | body.position.setZ(z) 266 | headSegment.position.setY(headTranslateY) 267 | bodyScaleSegment.scale.set(bodyScaleXZ, bodyScaleY, bodyScaleXZ) 268 | } 269 | ) 270 | 271 | // y轴上升段、下降段 272 | const rangeHeight = Math.max(this.world.width / 3, rangeH) + propHeight 273 | const yUp = animate( 274 | { 275 | from: { y: startY }, 276 | to: { y: rangeHeight }, 277 | duration: duration * .65, 278 | easing: TWEEN.Easing.Cubic.Out, 279 | autoStart: false 280 | }, 281 | ({ y }) => { 282 | body.position.setY(y) 283 | } 284 | ) 285 | const yDown = animate( 286 | { 287 | from: { y: rangeHeight }, 288 | to: { y: propHeight }, 289 | duration: duration * .35, 290 | easing: TWEEN.Easing.Cubic.In, 291 | autoStart: false 292 | }, 293 | ({ y }) => { 294 | body.position.setY(y) 295 | } 296 | ) 297 | 298 | // 落地后,生成下一个方块 -> 移动镜头 -> 更新关心的盒子 -> 结束 299 | const ended = () => { 300 | const { world } = this 301 | world.createProp() 302 | world.moveCamera() 303 | 304 | this.currentProp = nextProp 305 | this.nextProp = nextProp.getNext() 306 | // 跳跃结束了 307 | this.jumping = false 308 | } 309 | // 落地缓冲段 310 | const bufferUp = animate( 311 | { 312 | from: { s: .8 }, 313 | to: { s: 1 }, 314 | duration: 100, 315 | autoStart: false 316 | }, 317 | ({ s }) => { 318 | bodyScaleSegment.scale.setY(s) 319 | }, 320 | () => { 321 | // 以落地缓冲结束作为跳跃结束时间点 322 | ended() 323 | } 324 | ) 325 | 326 | // 上升 -> 下降 -> 落地缓冲 327 | yDown.chain(bufferUp) 328 | yUp.chain(yDown).start() 329 | 330 | // 需要处理不同方向空翻 331 | const direction = currentProp.getPosition().z === nextProp.getPosition().z 332 | this.flip(duration, direction) 333 | 334 | // 从起跳开始就回弹 335 | currentProp.springbackTransition(500) 336 | } 337 | 338 | stage.render() 339 | } 340 | 341 | // 空翻 342 | flip (duration, direction) { 343 | const { bodyRotateSegment } = this 344 | let increment = 0 345 | 346 | animate( 347 | { 348 | from: { deg: 0 }, 349 | to: { deg: 360 }, 350 | duration, 351 | easing: TWEEN.Easing.Sinusoidal.InOut 352 | }, 353 | ({ deg }) => { 354 | if (direction) { 355 | bodyRotateSegment.rotateZ(-(deg - increment) * (Math.PI/180)) 356 | } else { 357 | bodyRotateSegment.rotateX((deg - increment) * (Math.PI/180)) 358 | } 359 | increment = deg 360 | } 361 | ) 362 | } 363 | } 364 | 365 | export default LittleMan -------------------------------------------------------------------------------- /examples/js/wx-jump-stage1.c74513ec.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["wx-jump-stage1"],{"8bb1":function(e,t,i){},a56b:function(e,t,i){"use strict";var r=i("8bb1"),a=i.n(r);a.a},e424:function(e,t,i){"use strict";i.r(t);var r=function(){var e=this,t=e.$createElement;e._self._c;return e._m(0)},a=[function(){var e=this,t=e.$createElement,i=e._self._c||t;return i("div",{staticClass:"jump-world"},[i("canvas",{attrs:{id:"jump-world-canvas"}})])}],n=(i("ac6a"),i("768b")),o=i("d225"),s=i("b0b4"),h=i("5a89"),c=i("22b5"),u=Math.random,l=Math.sqrt,p=Math.floor,d=Math.pow,g=Math.sin,v=Math.cos,f=Math.tan,y=Math.PI;function m(e,t,i,r,a,n){var o=e*(y/180),s=t*(y/180),h=v(o)*r,c=g(o)*(n-a-i/f(o));if(!(h>c)){var u=h+(c-h)/2,l=u/f(o),p=g(s)*l,d=v(s)*l;return{x:p,y:u,z:d}}console.warn("警告: 垂直角度太小了!")}var w=new h["MeshLambertMaterial"],b=new h["BoxBufferGeometry"],S=function(e){return e[p(u()*e.length)]},P=function(e,t){return p(u()*(t-e+1))+e},x=function e(t){var i=e.box3||(e.box3=new h["Box3"]);return i.setFromObject(t),i.getSize(new h["Vector3"])},z=function(e,t,i){var r=g(2*t),a=g(t),n=d(e,2)*r/i,o=d(e*a,2)/(2*i);return{rangeR:n,rangeH:o}},O=function(e,t,i){var r=t.x,a=t.z,n=i.x,o=i.z,s=n-r,h=o-a,c=l(d(h,2)+d(s,2)),u=s*e/c,p=h*e/c;return{jumpDownX:r+u,jumpDownZ:a+p}},j=function(e,t,i){var r=e.from,a=e.to,n=e.duration,o=e.easing,s=void 0===o?function(e){return e}:o,h=e.autoStart,u=void 0===h||h,l=new c["a"].Tween(r).to(a,n).easing(s).onUpdate(t).onComplete((function(){i&&i()}));return u&&l.start(),k(),l},k=function e(){if(!e.openin){e.openin=!0;var t=function t(){var i=requestAnimationFrame(t);c["a"].update()||(e.openin=!1,cancelAnimationFrame(i))};t()}},C=(i("8e6e"),i("456d"),i("bd86"));function M(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,r)}return i}function Y(e){for(var t=1;t-1&&t[e]&&t[e]()||S(t)()}},{key:"createPropCreator",value:function(e,t){var i=this;if(Array.isArray(e))e.forEach((function(e){return i.createPropCreator(e,t)}));else{var r=this.propCreators,a=this.propSizeRange,n=this.propHeight;if(!(r.indexOf(e)>-1)){var o=function i(){if(t&&i.box)return i.box.clone();var r=e(h,{propSizeRange:a,propHeight:n,baseMeshLambertMaterial:w,baseBoxBufferGeometry:b});return t&&(i.box=r),r};r.push(o)}}}}]),e}(),E=L,B=i("75fc"),V=function(){function e(t){var i=t.world,r=t.stage,a=t.body,n=t.height,s=t.enterHeight,h=t.distanceRange,c=t.prev;Object(o["a"])(this,e),this.world=i,this.stage=r,this.body=a,this.height=n,this.enterHeight=s,this.distanceRange=h,this.prev=c}return Object(s["a"])(e,[{key:"computeMyPosition",value:function(){var e=this.world,t=this.prev,i=this.distanceRange,r=this.enterHeight,a={x:0,y:r,z:0};if(!t)return a;if(0===r)return a.z=e.width/2,a;var n=t.getPosition(),o=n.x,s=n.z,h=0===Math.round(Math.random()),c=t.getSize(),u=c.x,l=c.z,p=this.getSize(),d=p.x,g=p.z,v=P.apply(void 0,Object(B["a"])(i));return h?(a.x=o+u/2+v+d/2,a.z=s):(a.x=o,a.z=s+l/2+v+g/2),a}},{key:"enterStage",value:function(){var e=this.stage,t=this.body,i=this.height,r=this.computeMyPosition(),a=r.x,n=r.y,o=r.z;t.castShadow=!0,t.receiveShadow=!0,t.position.set(a,n,o),t.geometry.translate(0,i/2,0),e.add(t),e.render(),this.entranceTransition()}},{key:"entranceTransition",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:400,t=this.body,i=this.enterHeight,r=this.stage;0!==i&&j({to:{y:0},from:{y:i},duration:e,easing:c["a"].Easing.Bounce.Out},(function(e){var i=e.y;t.position.setY(i),r.render()}))}},{key:"springbackTransition",value:function(e){var t=this.body,i=this.stage,r=t.scale.y;j({from:{y:r},to:{y:1},duration:e,easing:c["a"].Easing.Bounce.Out},(function(e){var r=e.y;t.scale.setY(r),i.render()}))}},{key:"setNext",value:function(e){this.next=e}},{key:"getNext",value:function(e){return this.next}},{key:"getPosition",value:function(){return this.body.position}},{key:"setPosition",value:function(e,t,i){return this.body.position.set(e,t,i)}},{key:"scaleY",value:function(e){return this.body.scale.setY(e)}},{key:"getSize",value:function(){return x(this.body)}},{key:"dispose",value:function(){var e=this.body,t=this.stage,i=this.prev,r=this.next;this.prev=null,this.next=null,i&&(i.next=null),r&&(r.prev=null),e.geometry.dispose(),e.material.dispose(),t.remove(e)}}]),e}(),X=V;function Z(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,r)}return i}function N(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:100,t=(this.height,this.propCreator),i=this.propHeight,r=Object(n["a"])(this.propSizeRange,2),a=r[0],o=r[1],s=this.propDistanceRange,h=this.stage,c=this.props,u=this.props.length,l=c[u-1],p=new X({world:this,stage:h,body:t.createProp(u<3?0:-1),height:i,prev:l,enterHeight:e,distanceRange:s}),d=p.getSize();d.y!==i&&console.warn("高度: ".concat(d.y,",盒子高度必须为 ").concat(i)),(d.xo)&&console.warn("宽度: ".concat(d.x,", 盒子宽度必须为 ").concat(a," - ").concat(o)),(d.zo)&&console.warn("深度: ".concat(d.z,", 盒子深度度必须为 ").concat(a," - ").concat(o)),l&&l.setNext(p),p.enterStage(),c.push(p)}},{key:"moveCamera",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:500,i=this.stage,r=this.height,a=this.cameraInitalPosition,n=a.x,o=a.y,s=a.z,h=this.lightInitalPosition,c=h.x,u=h.y,l=h.z,p=r/10,d=this.getLastTwoCenterPosition(),g=d.x,v=d.y,f=d.z,y={x:g+n+p,y:o,z:f+s+p},m={x:g+c,y:u,z:f+l},w={cameraTo:y,lightTo:m,center:{x:g,y:v,z:f}};i.moveCamera(w,(function(){e.clearProps()}),t)}},{key:"getLastTwoCenterPosition",value:function(){var e=this.props,t=this.props.length,i=e[t-2].getPosition(),r=i.x,a=i.z,n=e[t-1].getPosition(),o=n.x,s=n.z;return{x:r+(o-r)/2,z:a+(s-a)/2}}},{key:"clearProps",value:function(){this.width,this.height;var e=this.safeClearLength,t=this.props,i=(this.stage,this.props.length),r=4;i>e&&(t.slice(0,r).forEach((function(e){return e.dispose()})),this.props=t.slice(r))}},{key:"computeSafeClearLength",value:function(){var e=this.width,t=this.height,i=this.propSizeRange,r=i[0],a=Math.sqrt(r*r+r*r);this.safeClearLength=Math.ceil(e/r)+Math.ceil(t/a/2)+1}}]),e}(),U=q,_={mounted:function(){new U({container:document.querySelector(".jump-world"),canvas:document.querySelector("#jump-world-canvas"),axesHelper:!0})}},J=_,W=(i("a56b"),i("2877")),$=Object(W["a"])(J,r,a,!1,null,"0bf23ea4",null);t["default"]=$.exports}}]); -------------------------------------------------------------------------------- /src/games/wx-jump-2/LittleMan.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import TWEEN from '@tweenjs/tween.js' 3 | 4 | import { 5 | baseMeshLambertMaterial, 6 | computeObligueThrowValue, 7 | computePositionByRangeR, 8 | animate, 9 | destroyMesh, 10 | getHitValidator 11 | } from './utils' 12 | import Particle from './Particle' 13 | 14 | class LittleMan { 15 | constructor ({ 16 | world, 17 | color, 18 | G 19 | }) { 20 | this.world = world 21 | this.color = color 22 | this.G = G 23 | this.v0 = world.width / 10 24 | this.theta = 90 25 | 26 | this.headSegment = null 27 | this.bodyScaleSegment = null 28 | this.bodyRotateSegment = null 29 | this.body = null 30 | 31 | this.unbindFunc = null 32 | this.currentProp = null 33 | this.nextProp = null 34 | this.powerStorageDuration = 1500 35 | 36 | this.stage = null 37 | 38 | this.createBody() 39 | this.particle = new Particle({ 40 | triggerObject: this.body, 41 | world 42 | }) 43 | this.resetPowerStorageParameter() 44 | } 45 | 46 | // 销毁小人 47 | destroy () { 48 | this.particle.destroy() 49 | destroyMesh(this.headSegment) 50 | destroyMesh(this.bodyScaleSegment) 51 | destroyMesh(this.bodyRotateSegment) 52 | destroyMesh(this.body) 53 | 54 | this.unbindFunc && this.unbindFunc() 55 | this.world = null 56 | this.particle = null 57 | this.headSegment = null 58 | this.bodyScaleSegment = null 59 | this.bodyRotateSegment = null 60 | this.body = null 61 | this.unbindFunc = null 62 | 63 | this.currentProp = null 64 | this.nextProp = null 65 | 66 | this.stage = null 67 | } 68 | 69 | bindEvent () { 70 | const { canvas } = this.world 71 | const isMobile = 'ontouchstart' in document 72 | const mousedownName = isMobile ? 'touchstart' : 'mousedown' 73 | const mouseupName = isMobile ? 'touchend' : 'mouseup' 74 | 75 | // 该起跳了 76 | const mouseup = () => { 77 | if (this.jumping) { 78 | return 79 | } 80 | this.jumping = true 81 | // 蓄力动作应该停止 82 | this.poweringUp = false 83 | // 停止粒子流 84 | this.particle.stopRunParticleFlow() 85 | 86 | this.jump() 87 | canvas.removeEventListener(mouseupName, mouseup) 88 | } 89 | 90 | // 蓄力的时候 91 | const mousedown = event => { 92 | event.preventDefault() 93 | // 跳跃没有完成不能操作 94 | if (this.poweringUp || this.jumping || !this.currentProp || this.gameOver) { 95 | return 96 | } 97 | this.poweringUp = true 98 | // 开启粒子流 99 | this.particle.runParticleFlow() 100 | this.powerStorage() 101 | canvas.addEventListener(mouseupName, mouseup, false) 102 | } 103 | 104 | canvas.addEventListener(mousedownName, mousedown, false) 105 | 106 | return () => { 107 | canvas.removeEventListener(mousedownName, mousedown) 108 | canvas.removeEventListener(mouseupName, mouseup) 109 | } 110 | } 111 | 112 | // 创建身体 113 | createBody () { 114 | const { color, world: { width } } = this 115 | const material = baseMeshLambertMaterial.clone() 116 | material.setValues({ color }) 117 | 118 | // 头部 119 | const headSize = this.headSize = width * .03 120 | const headTranslateY = this.headTranslateY = headSize * 4.5 121 | const headGeometry = new THREE.SphereGeometry(headSize, 16, 16) 122 | const headSegment = this.headSegment = new THREE.Mesh(headGeometry, material) 123 | headSegment.castShadow = true 124 | headSegment.translateY(headTranslateY) 125 | 126 | // 身体 127 | this.width = headSize * 1.2 * 2 128 | this.height = headSize * 5 129 | this.bodySize = headSize * 4 130 | const bodyBottomGeometry = new THREE.CylinderBufferGeometry(headSize * .9, this.width / 2, headSize * 2.5, 16) 131 | bodyBottomGeometry.translate(0, headSize * 1.25, 0) 132 | const bodyCenterGeometry = new THREE.CylinderBufferGeometry(headSize, headSize * .9, headSize, 16) 133 | bodyCenterGeometry.translate(0, headSize * 3, 0) 134 | const bodyTopGeometry = new THREE.SphereGeometry(headSize, 16, 16) 135 | bodyTopGeometry.translate(0, headSize * 3.5, 0) 136 | 137 | const bodyGeometry = new THREE.Geometry() 138 | bodyGeometry.merge(bodyTopGeometry) 139 | bodyGeometry.merge(new THREE.Geometry().fromBufferGeometry(bodyCenterGeometry)) 140 | bodyGeometry.merge(new THREE.Geometry().fromBufferGeometry(bodyBottomGeometry)) 141 | 142 | // 缩放控制 143 | const translateY = this.bodyTranslateY = headSize * 1.5 144 | const bodyScaleSegment = this.bodyScaleSegment = new THREE.Mesh(bodyGeometry, material) 145 | bodyScaleSegment.castShadow = true 146 | bodyScaleSegment.translateY(-translateY) 147 | 148 | // 旋转控制 149 | const bodyRotateSegment = this.bodyRotateSegment = new THREE.Group() 150 | bodyRotateSegment.add(headSegment) 151 | bodyRotateSegment.add(bodyScaleSegment) 152 | bodyRotateSegment.translateY(translateY) 153 | 154 | // 整体身高 = 头部位移 + 头部高度 / 2 = headSize * 5 155 | const body = this.body = new THREE.Group() 156 | body.add(bodyRotateSegment) 157 | } 158 | 159 | // 进入舞台 160 | enterStage (stage, { x, y, z }, nextProp) { 161 | const { body } = this 162 | 163 | body.position.set(x, y, z) 164 | 165 | this.stage = stage 166 | // 进入舞台时告诉小人目标 167 | this.nextProp = nextProp 168 | 169 | stage.add(body) 170 | stage.render() 171 | this.unbindFunc = this.bindEvent() 172 | } 173 | 174 | resetPowerStorageParameter () { 175 | // 由于蓄力导致的变形,需要记录后,在空中将小人复原 176 | this.toValues = { 177 | headTranslateY: 0, 178 | bodyScaleXZ: 0, 179 | bodyScaleY: 0 180 | } 181 | this.fromValues = this.fromValues || { 182 | headTranslateY: this.headTranslateY, 183 | bodyScaleXZ: 1, 184 | bodyScaleY: 1, 185 | propScaleY: 1 186 | } 187 | } 188 | 189 | // 蓄力 190 | powerStorage () { 191 | const { 192 | stage, powerStorageDuration, 193 | body, bodyScaleSegment, headSegment, 194 | bodySize, 195 | fromValues, 196 | currentProp, 197 | world: { propHeight } 198 | } = this 199 | 200 | this.powerStorageTime = Date.now() 201 | this.resetPowerStorageParameter() 202 | 203 | const tween = animate( 204 | { 205 | from: { ...fromValues }, 206 | to: { 207 | headTranslateY: bodySize - bodySize * .6, 208 | bodyScaleXZ: 1.3, 209 | bodyScaleY: .6, 210 | propScaleY: .6 211 | }, 212 | duration: powerStorageDuration 213 | }, 214 | ({ headTranslateY, bodyScaleXZ, bodyScaleY, propScaleY }) => { 215 | if (!this.poweringUp) { 216 | // 抬起时停止蓄力 217 | tween.stop() 218 | } else { 219 | 220 | headSegment.position.setY(headTranslateY) 221 | bodyScaleSegment.scale.set(bodyScaleXZ, bodyScaleY, bodyScaleXZ) 222 | currentProp.scaleY(propScaleY) 223 | body.position.setY(propHeight * propScaleY) 224 | 225 | // 保存此时的位置用于复原 226 | this.toValues = { 227 | headTranslateY, 228 | bodyScaleXZ, 229 | bodyScaleY 230 | } 231 | 232 | stage.render() 233 | } 234 | } 235 | ) 236 | } 237 | 238 | computePowerStorageValue () { 239 | const { powerStorageDuration, powerStorageTime, v0, theta } = this 240 | const diffTime = Date.now() - powerStorageTime 241 | const time = Math.min(diffTime, powerStorageDuration) 242 | const percentage = time / powerStorageDuration 243 | 244 | return { 245 | v0: v0 + 30 * percentage, 246 | theta: theta - 40 * percentage 247 | } 248 | } 249 | 250 | // 跳跃 251 | jump () { 252 | const { 253 | stage, body, 254 | currentProp, nextProp, 255 | world: { propHeight } 256 | } = this 257 | const duration = 400 258 | const start = body.position 259 | const target = nextProp.getPosition() 260 | const { x: startX, y: startY, z: startZ } = start 261 | 262 | // 开始游戏时,小人从第一个盒子正上方入场做弹球下落 263 | if (!currentProp && startX === target.x && startZ === target.z) { 264 | animate( 265 | { 266 | from: { y: startY }, 267 | to: { y: propHeight }, 268 | duration, 269 | easing: TWEEN.Easing.Bounce.Out 270 | }, 271 | ({ y }) => { 272 | body.position.setY(y) 273 | stage.render() 274 | }, 275 | () => { 276 | this.currentProp = nextProp 277 | this.nextProp = nextProp.getNext() 278 | this.jumping = false 279 | } 280 | ) 281 | } else { 282 | if (!currentProp) { 283 | return 284 | } 285 | 286 | const { bodyScaleSegment, headSegment, G, world, width } = this 287 | const { v0, theta } = this.computePowerStorageValue() 288 | const { rangeR, rangeH } = computeObligueThrowValue(v0, theta * (Math.PI / 180), G) 289 | const { jumpDownX, jumpDownZ } = computePositionByRangeR(rangeR, start, target) 290 | 291 | // 水平匀速 292 | animate( 293 | { 294 | from: { 295 | x: startX, 296 | z: startZ, 297 | ...this.toValues 298 | }, 299 | to: { 300 | x: jumpDownX, 301 | z: jumpDownZ, 302 | ...this.fromValues 303 | }, 304 | duration 305 | }, 306 | ({ x, z, headTranslateY, bodyScaleXZ, bodyScaleY }) => { 307 | body.position.setX(x) 308 | body.position.setZ(z) 309 | headSegment.position.setY(headTranslateY) 310 | bodyScaleSegment.scale.set(bodyScaleXZ, bodyScaleY, bodyScaleXZ) 311 | } 312 | ) 313 | 314 | // y轴上升段、下降段 315 | const rangeHeight = Math.max(world.width / 3, rangeH) + propHeight 316 | const yUp = animate( 317 | { 318 | from: { y: startY }, 319 | to: { y: rangeHeight }, 320 | duration: duration * .65, 321 | easing: TWEEN.Easing.Cubic.Out, 322 | autoStart: false 323 | }, 324 | ({ y }) => { 325 | body.position.setY(y) 326 | } 327 | ) 328 | const yDown = animate( 329 | { 330 | from: { y: rangeHeight }, 331 | to: { y: propHeight }, 332 | duration: duration * .35, 333 | easing: TWEEN.Easing.Cubic.In, 334 | autoStart: false 335 | }, 336 | ({ y }) => { 337 | body.position.setY(y) 338 | }, 339 | () => yDownCallBack() 340 | ) 341 | 342 | yUp.chain(yDown).start() 343 | 344 | // 空翻 345 | this.flip(duration) 346 | // 从起跳开始就回弹 347 | currentProp.springbackTransition(500) 348 | 349 | // 落地后的回调 350 | const yDownCallBack = () => { 351 | const currentInfos = currentProp.computePointInfos(width, jumpDownX, jumpDownZ) 352 | const nextInfos = nextProp.computePointInfos(width, jumpDownX, jumpDownZ) 353 | 354 | // 没有落在任何一个盒子上方 355 | if (!currentInfos.contains && !nextInfos.contains) { 356 | // gameOver 游戏结束,跌落 357 | console.log('GameOver') 358 | this.fall(currentInfos, nextInfos) 359 | } else { 360 | bufferUp.onComplete(() => { 361 | if (nextInfos.contains) { 362 | // 落在下一个盒子才更新场景 363 | // 落地后,生成下一个方块 -> 移动镜头 -> 更新关心的盒子 -> 结束 364 | world.createProp() 365 | world.moveCamera() 366 | 367 | this.currentProp = nextProp 368 | this.nextProp = nextProp.getNext() 369 | } 370 | 371 | // 粒子喷泉 372 | this.particle.runParticleFountain() 373 | // 跳跃结束了 374 | this.jumping = false 375 | }).start() 376 | } 377 | } 378 | 379 | // 落地缓冲段 380 | const bufferUp = animate( 381 | { 382 | from: { s: .8 }, 383 | to: { s: 1 }, 384 | duration: 100, 385 | autoStart: false 386 | }, 387 | ({ s }) => { 388 | bodyScaleSegment.scale.setY(s) 389 | } 390 | ) 391 | } 392 | } 393 | 394 | // 空翻 395 | flip (duration) { 396 | const { currentProp, bodyRotateSegment } = this 397 | let increment = 0 398 | 399 | animate( 400 | { 401 | from: { deg: 0 }, 402 | to: { deg: 360 }, 403 | duration, 404 | easing: TWEEN.Easing.Sinusoidal.InOut 405 | }, 406 | ({ deg }) => { 407 | if (currentProp.nextDirection === 'x') { 408 | bodyRotateSegment.rotateZ(-(deg - increment) * (Math.PI/180)) 409 | } else { 410 | bodyRotateSegment.rotateX((deg - increment) * (Math.PI/180)) 411 | } 412 | increment = deg 413 | } 414 | ) 415 | } 416 | 417 | // 跌落 418 | fall (currentInfos, nextInfos) { 419 | const { 420 | stage, body, currentProp, nextProp, 421 | world: { propHeight } 422 | } = this 423 | // 跳跃方向 424 | const direction = currentProp.nextDirection 425 | let degY, translateZ, 426 | validateProp, // 需要检测的盒子 427 | isForward // 相对方向的前、后 428 | 429 | if (currentInfos.isEdge && nextInfos.isEdge) { 430 | // 同时在2个盒子边缘 431 | return 432 | } else if (currentInfos.isEdge) { 433 | // 当前盒子边缘 434 | degY = currentInfos.degY 435 | translateZ = currentInfos.translateZ 436 | validateProp = nextProp 437 | isForward = true 438 | } else if (nextInfos.isEdge) { 439 | // 目标盒子边缘 440 | degY = nextInfos.degY 441 | translateZ = nextInfos.translateZ 442 | // 目标盒子边缘可能是在盒子前方或盒子后方 443 | if (direction === 'z') { 444 | isForward = degY < 90 && degY > 270 445 | } else { 446 | isForward = degY < 180 447 | } 448 | validateProp = isForward ? null : currentProp 449 | } else { 450 | // 空中掉落 451 | return animate( 452 | { 453 | from: { y: propHeight }, 454 | to: { y: 0 }, 455 | duration: 400, 456 | easing: TWEEN.Easing.Bounce.Out 457 | }, 458 | ({ y }) => { 459 | body.position.setY(y) 460 | stage.render() 461 | } 462 | ) 463 | } 464 | 465 | // 将粒子销毁掉 466 | this.particle.destroy() 467 | 468 | const { 469 | bodyRotateSegment, bodyScaleSegment, 470 | headSegment, bodyTranslateY, 471 | width, height 472 | } = this 473 | const halfWidth = width / 2 474 | 475 | // 将旋转原点放在脚下,同时让小人面向跌落方向 476 | headSegment.translateY(bodyTranslateY) 477 | bodyScaleSegment.translateY(bodyTranslateY) 478 | bodyRotateSegment.translateY(-bodyTranslateY) 479 | bodyRotateSegment.rotateY(degY * (Math.PI / 180)) 480 | 481 | // 将旋转原点移动到支撑点 482 | headSegment.translateZ(translateZ) 483 | bodyScaleSegment.translateZ(translateZ) 484 | bodyRotateSegment.translateZ(-translateZ) 485 | 486 | let incrementZ = 0 487 | let incrementDeg = 0 488 | let incrementY = 0 489 | 490 | let hitValidator = validateProp && getHitValidator(validateProp.body, direction, isForward) 491 | 492 | // 第一段 先沿着支点旋转 493 | const rotate = animate( 494 | { 495 | from: { 496 | degY: 0 497 | }, 498 | to: { 499 | degY: 90 500 | }, 501 | duration: 500, 502 | autoStart: false, 503 | easing: TWEEN.Easing.Quintic.In 504 | }, 505 | ({ degY }) => { 506 | if (hitValidator && hitValidator(body.children[0])) { 507 | rotate.stop() 508 | hitValidator = null 509 | } else { 510 | bodyRotateSegment.rotateX((degY - incrementDeg) * (Math.PI / 180)) 511 | incrementDeg = degY 512 | stage.render() 513 | } 514 | } 515 | ) 516 | // 第二段 跌落,沿z轴下落,沿y轴向外侧偏移 517 | const targZ = propHeight - halfWidth - translateZ 518 | const fall = animate( 519 | { 520 | from: { 521 | y: 0, 522 | z: 0 523 | }, 524 | to: { 525 | y: halfWidth - translateZ, 526 | z: targZ, 527 | }, 528 | duration: 300, 529 | autoStart: false, 530 | easing: TWEEN.Easing.Bounce.Out 531 | }, 532 | ({ z, y }) => { 533 | if (hitValidator && hitValidator(body.children[0])) { 534 | fall.stop() 535 | 536 | // 稍微处理一下,头撞到盒子的情况 537 | const radian = Math.atan((targZ - z) / height) 538 | if (isForward && direction === 'z') { 539 | bodyRotateSegment.translateY(-height) 540 | body.position.z += height 541 | body.rotateX(-radian) 542 | } else if (direction === 'z') { 543 | bodyRotateSegment.translateY(-height) 544 | body.position.z -= height 545 | body.rotateX(radian) 546 | } else if (isForward && direction === 'x') { 547 | bodyRotateSegment.translateY(-height) 548 | body.position.x += height 549 | body.rotateZ(radian) 550 | } else if (direction === 'x') { 551 | bodyRotateSegment.translateY(-height) 552 | body.position.x -= height 553 | body.rotateZ(-radian) 554 | } 555 | stage.render() 556 | hitValidator = null 557 | } else { 558 | headSegment.translateZ(z - incrementZ) 559 | bodyScaleSegment.translateZ(z - incrementZ) 560 | bodyRotateSegment.translateY(y - incrementY) 561 | incrementZ = z 562 | incrementY = y 563 | stage.render() 564 | } 565 | } 566 | ) 567 | 568 | rotate.chain(fall).start() 569 | } 570 | } 571 | 572 | export default LittleMan -------------------------------------------------------------------------------- /examples/js/wx-jump-stage2.87b8f792.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["wx-jump-stage2"],{"02f4":function(t,e,i){var n=i("4588"),r=i("be13");t.exports=function(t){return function(e,i){var a,o,s=String(r(e)),h=n(i),c=s.length;return h<0||h>=c?t?"":void 0:(a=s.charCodeAt(h),a<55296||a>56319||h+1===c||(o=s.charCodeAt(h+1))<56320||o>57343?t?s.charAt(h):a:t?s.slice(h,h+2):o-56320+(a-55296<<10)+65536)}}},"1c4c":function(t,e,i){"use strict";var n=i("9b43"),r=i("5ca1"),a=i("4bf8"),o=i("1fa8"),s=i("33a4"),h=i("9def"),c=i("f1ae"),l=i("27ee");r(r.S+r.F*!i("5cc5")((function(t){Array.from(t)})),"Array",{from:function(t){var e,i,r,u,p=a(t),g="function"==typeof this?this:Array,d=arguments.length,f=d>1?arguments[1]:void 0,v=void 0!==f,y=0,m=l(p);if(v&&(f=n(f,d>2?arguments[2]:void 0,2)),void 0==m||g==Array&&s(m))for(e=h(p.length),i=new g(e);e>y;y++)c(i,y,v?f(p[y],y):p[y]);else for(u=m.call(p),i=new g;!(r=u.next()).done;y++)c(i,y,v?o(u,f,[r.value,y],!0):r.value);return i.length=y,i}})},"386b":function(t,e,i){var n=i("5ca1"),r=i("79e5"),a=i("be13"),o=/"/g,s=function(t,e,i,n){var r=String(a(t)),s="<"+e;return""!==i&&(s+=" "+i+'="'+String(n).replace(o,""")+'"'),s+">"+r+""};t.exports=function(t,e){var i={};i[t]=e(s),n(n.P+n.F*r((function(){var e=""[t]('"');return e!==e.toLowerCase()||e.split('"').length>3})),"String",i)}},"5df3":function(t,e,i){"use strict";var n=i("02f4")(!0);i("01f9")(String,"String",(function(t){this._t=String(t),this._i=0}),(function(){var t,e=this._t,i=this._i;return i>=e.length?{value:void 0,done:!0}:(t=n(e,i),this._i+=t.length,{value:t,done:!1})}))},"673e":function(t,e,i){"use strict";i("386b")("sub",(function(t){return function(){return t(this,"sub","","")}}))},8448:function(t,e,i){},"8fad":function(t,e,i){"use strict";var n=i("8448"),r=i.n(n);r.a},cfd0:function(t,e,i){"use strict";i.r(e);var n=function(){var t=this,e=t.$createElement;t._self._c;return t._m(0)},r=[function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"jump-world"},[i("canvas",{attrs:{id:"jump-world-canvas"}})])}],a=i("768b"),o=(i("ac6a"),i("d225")),s=i("b0b4"),h=(i("673e"),i("5df3"),i("1c4c"),i("5a89")),c=i("22b5"),l=Math.random,u=Math.sqrt,p=Math.floor,g=Math.pow,d=Math.sin,f=Math.cos,v=Math.tan,y=Math.PI,m=0,w=function(){return m++},b=function(){return m=0},S=[6799930,15114812,16084076,9474969,4235007,16777215];function x(t,e,i,n,r,a){var o=t*(y/180),s=e*(y/180),h=f(o)*n,c=d(o)*(a-r-i/v(o));if(!(h>c)){var l=h+(c-h)/2,u=l/v(o),p=d(s)*u,g=f(s)*u;return{x:p,y:l,z:g}}console.warn("警告: 垂直角度太小了!")}var P=new h["MeshLambertMaterial"],A=new h["BoxBufferGeometry"](1,1,1,10,4,10);A.userData.type="box";var z=new h["CylinderBufferGeometry"](1,1,1,30,5);z.userData.type="Cylinder";var M=function(t){t.geometry&&(t.geometry.dispose(),t.geometry=null),t.material&&(t.material.dispose(),t.material=null),t.parent.remove(t),t.parent=null,t=null},j=function(t){return t[p(l()*t.length)]},Y=function(t,e){return p(l()*(e-t+1))+t},C=function t(e){var i=t.box3||(t.box3=new h["Box3"]);return i.setFromObject(e),i.getSize(new h["Vector3"])},R=function(t,e,i){var n=d(2*e),r=d(e),a=g(t,2)*n/i,o=g(t*r,2)/(2*i);return{rangeR:a,rangeH:o}},O=function(t,e,i){var n=t.position.clone(),r=t.geometry.attributes.position,a=r.count,o=Array.from({length:a}).map((function(t,e){return(new h["Vector3"]).fromBufferAttribute(r,e)})).filter((function(t){return"z"===e&&i?t.z<0:"z"===e?t.z>0:"x"===e&&i?t.x<0:"x"===e?t.x>0:void 0})).map((function(e){var i=e.applyMatrix4(t.matrix);return i.sub(t.position)}));return function(t){for(var e,i=0;e=o[i];i++){var r=new h["Raycaster"](n,e.clone().normalize()),a=r.intersectObject(t,!0);if(a.length>0&&a[0].distance-1&&e[t]&&e[t]()||j(e)()}},{key:"createPropCreator",value:function(t,e){var i=this;if(Array.isArray(t))t.forEach((function(t){return i.createPropCreator(t,e)}));else{var n=this.propCreators,r=this.propSizeRange,a=this.propHeight;if(!(n.indexOf(t)>-1)){var o=function i(){if(e&&i.box)return i.box.clone();var n=t(h,{propSizeRange:r,propHeight:a,baseMeshLambertMaterial:P,baseBoxBufferGeometry:A,baseCylinderBufferGeometry:z});return e&&(i.box=n),n};n.push(o)}}}}]),t}(),T=L,N=i("75fc"),U=function(){function t(e){var i=e.world,n=e.stage,r=e.body,a=e.height,s=e.enterHeight,h=e.distanceRange,c=e.prev;Object(o["a"])(this,t),this.world=i,this.stage=n,this.body=r,this.height=a,this.enterHeight=s,this.distanceRange=h,this.prev=c,this.nextDirection="",this.nextDistance=0}return Object(s["a"])(t,[{key:"computeMyPosition",value:function(){var t=this.world,e=this.prev,i=this.distanceRange,n=this.enterHeight,r={x:0,y:n,z:0};if(!e)return r;if(0===n)return r.z=t.width/2,e.nextDirection="z",r;var a=e.getPosition(),o=a.x,s=a.z,h=0===Math.round(Math.random()),c=e.getSize(),l=c.x,u=c.z,p=this.getSize(),g=p.x,d=p.z,f=Y.apply(void 0,Object(N["a"])(i));return h?(r.x=o+l/2+f+g/2,r.z=s,e.nextDirection="x"):(r.x=o,r.z=s+u/2+f+d/2,e.nextDirection="z"),e.nextDistance=f,r}},{key:"enterStage",value:function(){var t=this.stage,e=this.body,i=this.height,n=this.computeMyPosition(),r=n.x,a=n.y,o=n.z;e.castShadow=!0,e.receiveShadow=!0,e.position.set(r,a,o),e.geometry.translate(0,i/2,0),t.add(e),this.entranceTransition()}},{key:"entranceTransition",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:400,e=this.body,i=this.enterHeight,n=this.stage;0!==i&&G({to:{y:0},from:{y:i},duration:t,easing:c["a"].Easing.Bounce.Out},(function(t){var i=t.y;e.position.setY(i),n.render()}))}},{key:"springbackTransition",value:function(t){var e=this.body,i=this.stage,n=e.scale.y;G({from:{y:n},to:{y:1},duration:t,easing:c["a"].Easing.Bounce.Out},(function(t){var n=t.y;e.scale.setY(n),i.render()}))}},{key:"computePointInfos",value:function(t,e,i){var n=this.body;if(!n)return{};var r=n.geometry.userData.type,a=this.getSize(),o=a.x,s=a.z,h=this.getPosition(),c=h.x,l=h.z,u=t/2;if("box"===r){var p=o/2,g=s/2,d=c-p,f=c+p,v=l-g,y=l+g,m=e>=d&&e<=f&&i>=v&&i<=y;if(m)return{contains:m};var w=Math.abs(i-l)-g,b=Math.abs(e-c)-p;if(w>=u||b>=u)return{contains:m};var S=!1,x=0,P=0;return ed?(i>y&&iv-u&&(x=180),S=!0,P=w):iv&&(e>f&&ed-u&&(x=270),S=!0,P=b),{contains:m,translateZ:P,isEdge:S,degY:x}}var A=o/2,z=Math.sqrt(Math.pow(c-e,2)+Math.pow(l-i,2)),M=z<=A;if(M)return{contains:M};if(z>=A+u)return{contains:M};var j=!0,Y=z-A,C=180*Math.atan(Math.abs(e-c)/Math.abs(i-l))/Math.PI;return e===c?C=i>l?0:180:i===l?C=e>c?90:270:e>c&&i>l||(C=e>c&&i=c)e.x=0,e.y=a,e.z=0;else{var u=h-n,g=c-r,d=l-o,f=Math.sqrt(Math.pow(u,2)+Math.pow(g,2)+Math.pow(d,2)),v=p/f;e.y+=v*g,e.x+=v*u,e.z+=v*d}})),o.stage.render()}s=Date.now()}else i.runingParticleFountain=!1,cancelAnimationFrame(r)};l()}},{key:"runParticleFountain",value:function(){if(!this.runingParticleFountain){this.runingParticleFountain=!0;var t=this.particleSystem,e=this.quantity,i=this.initalY,n=t.children.filter((function(t){return t.position.y===i})).slice(0,e);if(n.length){var r=this.triggerObjectWidth,a=this.fountainRangeX,o=this.fountainSizeRange,s=this.fountainRangeY,h=r/2;n.forEach((function(t){var e=t.position,i=t.material,n=Y.apply(void 0,Object(N["a"])(a)),r=Y.apply(void 0,Object(N["a"])(a)),c=n<0?-h:h,l=r<0?-h:h;e.x=c+n,e.z=l+r,e.y=Y.apply(void 0,Object(N["a"])(s)),i.setValues({size:Y.apply(void 0,Object(N["a"])(o))})})),this.runParticleFountainPump(n,1e3)}}}}]),t}(),J=X;function W(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,n)}return i}function q(t){for(var e=1;e270:i<180,r=a?null:h}this.particle.destroy();var g=this.bodyRotateSegment,d=this.bodyScaleSegment,f=this.headSegment,v=this.bodyTranslateY,y=this.width,m=this.height,w=y/2;f.translateY(v),d.translateY(v),g.translateY(-v),g.rotateY(i*(Math.PI/180)),f.translateZ(n),d.translateZ(n),g.translateZ(-n);var b=0,S=0,x=0,P=r&&O(r.body,p,a),A=G({from:{degY:0},to:{degY:90},duration:500,autoStart:!1,easing:c["a"].Easing.Quintic.In},(function(t){var e=t.degY;P&&P(s.children[0])?(A.stop(),P=null):(g.rotateX((e-S)*(Math.PI/180)),S=e,o.render())})),z=u-w-n,M=G({from:{y:0,z:0},to:{y:w-n,z:z},duration:300,autoStart:!1,easing:c["a"].Easing.Bounce.Out},(function(t){var e=t.z,i=t.y;if(P&&P(s.children[0])){M.stop();var n=Math.atan((z-e)/m);a&&"z"===p?(g.translateY(-m),s.position.z+=m,s.rotateX(-n)):"z"===p?(g.translateY(-m),s.position.z-=m,s.rotateX(n)):a&&"x"===p?(g.translateY(-m),s.position.x+=m,s.rotateZ(n)):"x"===p&&(g.translateY(-m),s.position.x-=m,s.rotateZ(-n)),o.render(),P=null}else f.translateZ(e-b),d.translateZ(e-b),g.translateY(i-x),b=e,x=i,o.render()}));A.chain(M).start()}}}]),t}(),_=K,$=function(){function t(e){var i=e.container,n=e.canvas,r=e.needDefaultCreator,a=void 0===r||r,s=e.axesHelper,h=void 0!==s&&s;Object(o["a"])(this,t);var c=i.offsetWidth,l=i.offsetHeight;this.G=9.8,this.container=i,this.canvas=n,this.width=c,this.height=l,this.needDefaultCreator=a,this.axesHelper=h;var u=~~(c/6),p=~~(c/3.5);this.propSizeRange=[u,p],this.propHeight=~~(p/2),this.propDistanceRange=[~~(u/2),2*p],this.stage=null,this.propCreator=null,this.littleMans=[],this.props=[],this.init()}return Object(s["a"])(t,[{key:"init",value:function(){this.initStage(),this.initPropCreator(),this.computeSafeClearLength(),this.createProp(0),this.createProp(0),this.moveCamera(0),this.initLittleMan()}},{key:"destroy",value:function(){this.clear()}},{key:"clear",value:function(){this.stage&&this.stage.reset(),this.props.forEach((function(t){return t.destroy()})),this.props=[],b(),this.littleMans.forEach((function(t){return t.destroy()})),this.littleMans=[]}},{key:"initStage",value:function(){var t=this.canvas,e=this.axesHelper,i=this.width,n=this.height,r=.1,a=2e3,o=this.cameraInitalPosition=x(35,225,n/2,n/2,r,a),s=this.lightInitalPosition={x:-300,y:600,z:200};this.stage=new F({width:i,height:n,canvas:t,axesHelper:e,cameraNear:r,cameraFar:a,cameraInitalPosition:o,lightInitalPosition:s})}},{key:"initPropCreator",value:function(){var t=this.needDefaultCreator,e=this.propSizeRange,i=this.propHeight;this.propCreator=new T({propHeight:i,propSizeRange:e,needDefaultCreator:t})}},{key:"createPropCreator",value:function(){var t;(t=this.propCreator).createPropCreator.apply(t,arguments)}},{key:"initLittleMan",value:function(){var t=this.stage,e=this.propHeight,i=this.G,n=this.props,r=new _({world:this,color:3696793,G:i});r.enterStage(t,{x:0,y:e+80,z:0},n[0]),r.jump(),this.littleMans.push(r)}},{key:"createProp",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:100,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1,i=this.propCreator,n=this.propHeight,r=Object(a["a"])(this.propSizeRange,2),o=r[0],s=r[1],h=this.propDistanceRange,c=this.stage,l=this.props,u=this.props.length,p=l[u-1],g=new V({world:this,stage:c,body:i.createProp(-1===e&&u<3?0:e),height:n,prev:p,enterHeight:t,distanceRange:h}),d=g.getSize();return d.y!==n&&console.warn("高度: ".concat(d.y,",盒子高度必须为 ").concat(n)),(d.xs)&&console.warn("宽度: ".concat(d.x,", 盒子宽度必须为 ").concat(o," - ").concat(s)),(d.zs)&&console.warn("深度: ".concat(d.z,", 盒子深度度必须为 ").concat(o," - ").concat(s)),p&&p.setNext(g),g.enterStage(),l.push(g),g}},{key:"moveCamera",value:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:500,i=this.stage,n=this.height,r=this.cameraInitalPosition,a=r.x,o=r.y,s=r.z,h=this.lightInitalPosition,c=h.x,l=h.y,u=h.z,p=n/10,g=this.getLastTwoCenterPosition(),d=g.x,f=g.y,v=g.z,y={x:d+a+p,y:o,z:v+s+p},m={x:d+c,y:l,z:v+u},w={cameraTo:y,lightTo:m,center:{x:d,y:f,z:v}};i.moveCamera(w,(function(){t.clearProps()}),e)}},{key:"getLastTwoCenterPosition",value:function(){var t=this.props,e=this.props.length,i=t[e-2].getPosition(),n=i.x,r=i.z,a=t[e-1].getPosition(),o=a.x,s=a.z;return{x:n+(o-n)/2,z:r+(s-r)/2}}},{key:"clearProps",value:function(){var t=this.safeClearLength,e=this.props,i=this.props.length,n=4;i>t&&(e.slice(0,n).forEach((function(t){return t.destroy()})),this.props=e.slice(n))}},{key:"computeSafeClearLength",value:function(){var t=this.width,e=this.height,i=this.propSizeRange,n=i[0],r=Math.sqrt(n*n+n*n);this.safeClearLength=Math.ceil(t/n)+Math.ceil(e/r/2)+1}}]),t}(),tt=$,et={mounted:function(){new tt({container:document.querySelector(".jump-world"),canvas:document.querySelector("#jump-world-canvas"),axesHelper:!0})}},it=et,nt=(i("8fad"),i("2877")),rt=Object(nt["a"])(it,n,r,!1,null,"9a0b2f7a",null);e["default"]=rt.exports},e8df:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REVGODRBRjIyRjU2MTFFNUExM0Y4OUExMTc4QTBCQUMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REVGODRBRjMyRjU2MTFFNUExM0Y4OUExMTc4QTBCQUMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpERUY4NEFGMDJGNTYxMUU1QTEzRjg5QTExNzhBMEJBQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpERUY4NEFGMTJGNTYxMUU1QTEzRjg5QTExNzhBMEJBQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgDz1z0AAAZeSURBVHja7FuLjuMoEASMnfz/167N67SSkepK1fgxyVxOM5YQzkxiu4vqorvBvrXmfvIR3A8/fgH4BeCHH/Gb7uOp4d/70egc2/8SgG5sMM79bpw3DK/G+ccD0A1VzYPbeYMB3dCyfy47ABXAqJ8IQDduMnpPQDhiAjOgGz7RZ+zbpwAQ9muh0ZMAIZArIHgNRrZSK9AC9eW/BqAbzr0CYSIdcMIF2HAG4G9Le9+vc5sN8YuUj2S4+tzPOwjeYEEjH1eG5934sJ9nuMYtbYgvoPxMxs8GGJMAQWmAGvlubGdRFuLav1ffDUAwDP7bL+JvigXBmAUqCB5TPgMD1DV6fwmEeIP2E418N3wW5zMxQemAJwCQ/t3oeTe8G78duJI7C0K86fPK4Ad8XgQrlBtgKF7JBdDnM7GImaSOU4HTFQAm4eMLtQecMxs6a5QOqKkv0ejHfeTx9yqIQjDzqwBA+jIL0PgHgPAwdEG5QROjX4D2m4glLNpz6Fy+CoAX09wMtO/GPg0QFtKLyaAvA5Bg9HkmYfFrYirl/jYAQYz+TCA8oX8KRihBDKQBTYz+Bt9fTwonfo5HYhgvqL4y3nIBBqMDxTNCB7gRABkYsAr6q8wRp9D+vP28WoIYT4w+NvTlxTD+Ce1BIKAgokGNRrEL4CZcpgnj+8h30UQGYN5wCQDM7iK1xRDCpwHCg0CIBgAsfkov2PhMzCkwbQZgwC0AJiPDi0YcsAyYwFowgQYgA7JQfncQLGHQlPfrc+g8KRDiAf09BR7TIAQe6cCTpkZWdJy3s/i/E25SgPLsLioTvcQAruqoCytBVGCwFixCCB35f6Jgh0d8AZGcd+MxYJqMuMGzGJ7RAIsF02CGWEQ88BBxAWtANmjfiCEJQIgAAhuuSnKnAOBCZhAXjiIp4vjfco0FAHBQ+8uGNhQa8UWM9jQYfasIc4oBfCG8QRQsUMAwK3A69DB/ZzAefX0WAmoVW9Rg+asAeNGCcIkgCqD8cJZGKAACjTqKokqpw8BgVXx1VwFwongZDgCaRJE0DqJJD/6PIz8ZBlvn3sgTrIWYS9mgAiQYrHDGYgizZIb0uIDx/D1vGMjGWRUibxl/NhdQCHIW58V6ox8Ag/0MIujpewy4M1g5eh4/AuHM4mg7WMPzRimKU1IrXU2UzDiRvLTBfdVzWZWhdoUBTZSr2sEa3shQVeltIHyYCvNvOAQ+um8bDNgpAJqBnlVssOr4FZS8QE0/QKUJZ4EELRvXO2rOAOhSOtwM1Jtx02K0LMrZHpRfxQEIQCIAswCkDICoRwXSMwywFiy4Z2OTKGZy5KciwZ4JrnSdRPc4A4haZj+tAZVWZa1RT/RA/NBc/uq/5VyAS2Er9UkskCgg1LOabjBiQDVq9UzvAumoSkw8pb0FortAFM1UCf4DAGCzgFCa0e6UxNQ6fT2ge9z7lSI4L/w80/+cKIchAxQQHaQsWj0LwpELFCgmjAzfoLdqeGXPAZQgqopQB1M1C4RiaEOxqsNHcYBVhcERtxIULwQ009qAYkAWYogsWEkXsnCLLJjb7gBQqKaGfTIqRdZqTxW/sxhQwKj1AITNmCmKEMHLuUAVIISD7S/OACAb9UAvFkYyjOyIBasAIBmCeGthBA0IULBIJwoOXOfHGkCk1eFm1AESCZ/FAOUCmUTw9tJYgYfNRp1NbXUpoogZDQa0gRBmMvZIFIsA4UuLozh9jXJzNfKFGMCu44QLNDJgo1kBg6M/9P+N4oJ6tEfgbEGkun/vylI5ttroiCWtRPSfxGoP7w8owrhVgDHSgJdtkEBXSO54kxPOxRF0Iw62tlQxGyTSg2REhhtpx6n9g1cAaFS18caoq5Ubq6DJ11HukwmEJJR/E37fXg2Ao+KlGxRIiliji5QbjPYJWnlHEukyn1/aNHlnm1yFao4Svgh9hAUNruQ6EQdYkae1XS6JjPTt+wQd3aQZDz+LkNmq26s9PtVY/s6DusC37RR1Is/GpGOiCDKI8JervFaZrRp5fxZ1RPedADiYs3Fbe3X3d4u3wb4fq974pXcHXrVdHtkQKHwOA/ELQkuUKxQDkI95XwDZ0Cs/1jqiWqywNjo2I639yDdGFBCj94XwnaE2ENWXvybzbgA4cHLu3FtjXL6u7s1vjL0bADdY2vqY4/fN0V8AfvjxjwADALR8rODQS303AAAAAElFTkSuQmCC"}}]); --------------------------------------------------------------------------------