├── .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 |
2 |
3 |
4 |
5 |
6 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
21 |
--------------------------------------------------------------------------------
/src/games/wx-jump-2/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
16 |
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 |
2 |
17 |
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+""+e+">"};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"}}]);
--------------------------------------------------------------------------------