├── README.md ├── ball.js ├── images └── sprite.png ├── index.html ├── index.js ├── storage.js ├── three ├── three.cjs ├── three.js ├── three.min.js ├── three.module.js └── three.module.min.js ├── v1.gif ├── v1.html ├── v2.1.gif ├── v2.gif └── win.js /README.md: -------------------------------------------------------------------------------- 1 | # mark-cross-window-render 2 | 3 | 跨窗口量子纠缠粒子效果 4 | 5 | v2.0 Demo: https://markz-demo.github.io/mark-cross-window-render/ 6 | 7 | ### v2.1 8 | 9 | 需要去掉`ball.js`文件顶部常量注释。 10 | 11 |  12 | 13 | ### v2.0 14 | 15 |  16 | 17 | ### v1.0 18 | 19 | 纯js实现Web跨窗口渲染动画效果 20 | 21 |  -------------------------------------------------------------------------------- /ball.js: -------------------------------------------------------------------------------- 1 | import * as THREE from './three/three.module.js'; 2 | 3 | // const OUTER_RADIUS = 150, INNER_RADIUS = 80; // 大小球体的半径 4 | // const OUTER_SPRITE_WIDTH = 50, INNER_RSPRITE_WIDTH = 20; // 大小球体上的粒子高宽 5 | // const OUTER_SPRITE_COUNT = 100, INNER_RSPRITE_COUNT = 50, MOVE_SPRITE_COUNT = 10; // 大小球体、及连线上的粒子个数 6 | // const MOVE_FRAME_NUM = 100; // 连线动画的粒子移动的帧数,数越小移动速度越快 7 | // const MOVE_FRAME_GAP = 10; // 连线动画的粒子先后移动的帧数间隔,数值越小越密集 8 | // const COLORS = [0, 60 / 360, 120 / 360, 180 / 360, 240 / 360]; // 粒子颜色数组,一个球体对应一个颜色 9 | 10 | // v2.1版小粒子效果 11 | 12 | const OUTER_RADIUS = 150, INNER_RADIUS = 80; // 大小球体的半径 13 | const OUTER_SPRITE_WIDTH = 10, INNER_RSPRITE_WIDTH = 5; // 大小球体上的粒子高宽 14 | const OUTER_SPRITE_COUNT = 300, INNER_RSPRITE_COUNT = 50, MOVE_SPRITE_COUNT = 200; // 大小球体、及连线上的粒子个数 15 | const MOVE_FRAME_NUM = 100; // 连线动画的粒子移动的帧数 16 | const MOVE_FRAME_GAP = 2; // 连线动画的粒子先后移动的帧数间隔,数值越小越密集 17 | const COLORS = [0, 60 / 360, 120 / 360, 180 / 360, 240 / 360]; // 粒子颜色数组,一个球体对应一个颜色 18 | 19 | 20 | // 创建粒子材质 21 | const materialTemp = (function () { 22 | // 创建纹理贴图 23 | const textureLoader = new THREE.TextureLoader(); 24 | const map = textureLoader.load('images/sprite.png'); 25 | map.colorSpace = THREE.SRGBColorSpace; 26 | // 精灵材质 27 | return new THREE.SpriteMaterial({ map: map, color: 0xffffff, fog: true }); 28 | })(); 29 | 30 | // 字体类,一个球体对应一个对象 31 | export default class BALL { 32 | 33 | camera; 34 | renderer; 35 | scene; 36 | key; 37 | group1; 38 | group2; 39 | offset; 40 | movePList = []; 41 | moving = false; 42 | destroyed = false; 43 | 44 | constructor(camera, renderer, scene, offset, key) { 45 | this.camera = camera; 46 | this.renderer = renderer; 47 | this.scene = scene; 48 | this.offset = offset; 49 | this.key = key; 50 | } 51 | 52 | // 实例化,渲染两个球体Group(一大一小)和连线粒子list 53 | init(keys) { 54 | this.group1 = new THREE.Group(); 55 | this.group2 = new THREE.Group(); 56 | for (let i = 0; i < OUTER_SPRITE_COUNT; i++) { 57 | const sprite = this.createSprite(); 58 | this.group1.add(sprite); 59 | } 60 | for (let i = 0; i < INNER_RSPRITE_COUNT; i++) { 61 | const sprite = this.createSprite(keys, i); 62 | this.group2.add(sprite); 63 | this.group2.visible = keys.length > 0; 64 | } 65 | this.scene.add(this.group1); 66 | this.scene.add(this.group2); 67 | 68 | this.movePList = []; 69 | // 除了自身,有几个其它球体,就实例化几组粒子,用来链接球体动画 70 | keys.forEach(k => { 71 | const groups = []; 72 | for (let i = 0; i < MOVE_SPRITE_COUNT; i++) { 73 | const sprite = this.createSprite(); 74 | const group = new THREE.Group(); 75 | group.visible = false; 76 | group.add(sprite); 77 | this.scene.add(group); 78 | groups.push(group); 79 | } 80 | this.movePList.push({ key: k, groups, moving: false, frame: 0, index: 0 }) 81 | }); 82 | } 83 | 84 | // 创建粒子对象 85 | createSprite(keys, i) { 86 | const x = Math.random() - 0.5; 87 | const y = Math.random() - 0.5; 88 | const z = Math.random() - 0.5; 89 | const inner = !!keys; 90 | const radius = !inner ? OUTER_RADIUS : INNER_RADIUS; 91 | const width = !inner ? OUTER_SPRITE_WIDTH : INNER_RSPRITE_WIDTH; 92 | let key = this.key; 93 | if (inner && keys.length > 0) { 94 | key = keys[i % keys.length]; // 如果除了当前球体,还有其它多个球体,需要将内圆球体粒子显示多种颜色 95 | } 96 | 97 | // clone材质 98 | const material = materialTemp.clone(); 99 | // 粒子颜色的对比度随机 100 | material.color.setHSL(COLORS[(key - 1) % 3], 0.75/* Math.random() */, 0.5); 101 | material.map.offset.set(- 0.5, - 0.5); 102 | material.map.repeat.set(2, 2); 103 | // 创建精灵 104 | const sprite = new THREE.Sprite(material); 105 | sprite.position.set(x, y, z); 106 | sprite.position.normalize(); 107 | sprite.position.multiplyScalar(radius); 108 | sprite.scale.set(width, width, 1.0); 109 | return sprite; 110 | } 111 | 112 | // 移除当前球体所有相关对象,并置为销毁状态 113 | remove() { 114 | this.scene.remove(this.group1); 115 | this.scene.remove(this.group2); 116 | this.movePList.forEach(({ groups }) => { 117 | groups.forEach(group => { 118 | this.scene.remove(group); 119 | }); 120 | }); 121 | this.destroyed = true; 122 | } 123 | 124 | // 渲染动画 125 | render(offset, movePs) { 126 | if (this.destroyed) return; 127 | // 渲染粒子绕球体3d旋转动画 128 | this.renderRotation(); 129 | 130 | // 如果球体不在窗口中心,需要按offset位移坐标 131 | if (offset) { 132 | this.offset = offset; 133 | this.group1.position.x = this.group2.position.x = this.offset.x; 134 | this.group1.position.y = this.group2.position.y = this.offset.y; 135 | } 136 | 137 | // 渲染球体之间连线动画 138 | this.renderMove(movePs); 139 | 140 | this.renderer.render(this.scene, this.camera); 141 | } 142 | 143 | // 渲染粒子绕球体3d旋转动画 144 | renderRotation() { 145 | const time = Date.now() / 1000; 146 | this.group1.children.forEach(sprite => { 147 | sprite.material.rotation -= 0.1; 148 | }); 149 | this.group1.rotation.x = this.group2.rotation.x = time * 0.5; 150 | this.group1.rotation.y = this.group2.rotation.y = time * 0.75; 151 | this.group1.rotation.z = this.group2.rotation.z = time * 1.0; 152 | 153 | // this.movePList.forEach(({ groups }) => { 154 | // groups.forEach(group => { 155 | // group.rotation.x = time * 0.5; 156 | // group.rotation.y = time * 0.75; 157 | // group.rotation.z = time * 1.0; 158 | // }); 159 | // }); 160 | } 161 | 162 | // 渲染球体之间连线动画 163 | renderMove(movePs) { 164 | const { group1, movePList } = this; 165 | // 如果除了当前球体还有别的多个球体,需要遍历其它球体数据,渲染连线动画 166 | movePs.forEach(({ key, moveP }) => { 167 | const move = movePList.find(item => item.key == key); 168 | 169 | // 如果两个球体重合了,停止连线动画,隐藏所有粒子 170 | if (!moveP || (Math.abs(moveP.x) <= OUTER_RADIUS * 2 && Math.abs(moveP.y) <= OUTER_RADIUS * 2)) { 171 | if (move.moving == true) { 172 | move.moving = false; 173 | move.frame = 0; 174 | move.index = 0; 175 | move.groups.forEach(group => { 176 | group.visible = false; 177 | this.clearGroupPosition(group); 178 | }); 179 | } 180 | return; 181 | } 182 | 183 | if ((move.p && (moveP.x != move.p.x || moveP.y != move.p.y)) || move.moving == false) { 184 | move.p = moveP; 185 | move.groups.forEach(group => { 186 | this.clearGroupPosition(group); 187 | }) 188 | move.moving = true; 189 | move.frame = 0; 190 | move.index = 0; 191 | } 192 | 193 | const moveX = moveP.x + group1.position.x; 194 | const moveY = moveP.y + group1.position.y; 195 | 196 | move.frame++; 197 | if (move.frame % MOVE_FRAME_GAP === 0) { // 每10帧开始移动一个粒子,达到粒子先后依次移动射线效果 198 | move.index++; 199 | } 200 | move.groups.forEach((group, i) => { 201 | group.index ||= 0; 202 | if (i > move.index) { 203 | return; 204 | } 205 | const gapX = Math.abs(moveX - group.position.x); 206 | const gapY = Math.abs(moveY - group.position.y); 207 | 208 | if (group.index == MOVE_FRAME_NUM) { // 粒子移动具体帧数后,重置位置,重新移动 209 | group.visible = false; 210 | group.index = 0; 211 | this.clearGroupPosition(group); 212 | } 213 | else if (gapX < 0.5 || gapY < 0.5) { // 粒子移动临近目的位置时,隐藏,等待帧数结束重新移动 214 | group.index++; 215 | group.visible = false; 216 | } 217 | else { 218 | group.index++; 219 | group.visible = true; 220 | group.position.x += moveP.x / MOVE_FRAME_NUM; // 改变粒子坐标,从球1到球2位置 221 | group.position.y += moveP.y / MOVE_FRAME_NUM; 222 | const radius = OUTER_RADIUS - group.index * (OUTER_RADIUS - INNER_RADIUS / 2) / MOVE_FRAME_NUM; 223 | const width = OUTER_SPRITE_WIDTH - group.index * (OUTER_SPRITE_WIDTH - INNER_RSPRITE_WIDTH) / MOVE_FRAME_NUM; 224 | group.children[0].position.normalize(); 225 | group.children[0].position.multiplyScalar(radius); // 改变粒子环绕半径,从大到小 226 | group.children[0].scale.set(width, width, 1.0); // 改变粒子高宽,从大到小 227 | } 228 | }); 229 | }) 230 | } 231 | 232 | // 重置所有连线粒子状态 233 | clearGroupPosition(group) { 234 | const { group1 } = this; 235 | group.index = 0; 236 | group.position.x = group1.position.x; 237 | group.position.y = group1.position.y; 238 | group.children[0].position.normalize(); 239 | group.children[0].position.multiplyScalar(OUTER_RADIUS); 240 | group.children[0].scale.set(OUTER_SPRITE_WIDTH, OUTER_SPRITE_WIDTH, 1.0); 241 | } 242 | } -------------------------------------------------------------------------------- /images/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markz-demo/mark-cross-window-render/45278c4b091163ca5cf2e32e0b55eb2aa33c270b/images/sprite.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 |