Simple game using Pixi.js optimization hints - javascript

I coded a little simulation where you can dig cubes, they fall an stack, you can consume some or make a selection explode.
I started this little project for fun and my aim is to handle as much cubes as possible (100k would be a good start).
The project is simple, 3 possibles actions:
Dig cubes (2k each click)
Consume cubes (50 each click)
Explode cubes (click on two points to form a rectangle)
Currently, on my computer, performance starts to drop when I got about 20k cubes. When you select a large portion of cubes to explode, performance are heavily slowed down too. I'm not sure the way I simplified physics is the best way to go.
Could you give me some hints on how to improve/optimize it ?
Here is the complete code : (The stacking doesn't work in the SO snippet so here is the codepen version)
(() => {
// Variables init
let defaultState = {
cubesPerDig : 2000,
cubesIncome : 0,
total : 0
};
let cubeSize = 2, dropSize = window.innerWidth, downSpeed = 5;
let state,
digButton, // Button to manually dig
gameState, // An object containing the state of the game
cubes, // Array containing all the spawned cubes
heightIndex, // fake physics
cubesPerX, // fake physics helper
playScene; // The gamescene
// App setup
let app = new PIXI.Application();
app.renderer.view.style.position = "absolute";
app.renderer.view.style.display = "block";
app.renderer.autoResize = true;
document.body.appendChild(app.view);
// Resize
function resize() {
app.renderer.resize(window.innerWidth, window.innerHeight);
}
window.onresize = resize;
resize();
// Hello ! we can talk in the chat.txt file
// Issue : When there are more than ~10k cubes, performance start to drop
// To test, click the "mine" button about 5-10 times
// Main state
function play(delta){
// Animate the cubes according to their states
let cube;
for(let c in cubes){
cube = cubes[c];
switch(cube.state) {
case STATE.LANDING:
// fake physics
if(!cube.landed){
if (cube.y < heightIndex[cube.x]) {
cube.y+= downSpeed;
}else if (cube.y >= heightIndex[cube.x]) {
cube.y = heightIndex[cube.x];
cube.landed = 1;
heightIndex[cube.x] -= cubeSize;
}
}
break;
case STATE.CONSUMING:
if(cube.y > -cubeSize){
cube.y -= cube.speed;
}else{
removeCube(c);
}
break;
case STATE.EXPLODING:
if(boundings(c)){
continue;
}
cube.x += cube.eDirX;
cube.y += cube.eDirY;
break;
}
}
updateUI();
}
// Game loop
function gameLoop(delta){
state(delta);
}
// Setup variables and gameState
function setup(){
state = play;
digButton = document.getElementById('dig');
digButton.addEventListener('click', mine);
playScene = new PIXI.Container();
gameState = defaultState;
/* User inputs */
// Mine
document.getElementById('consume').addEventListener('click', () => {consumeCubes(50)});
// Manual explode
let explodeOrigin = null
document.querySelector('canvas').addEventListener('click', e => {
if(!explodeOrigin){
explodeOrigin = {x: e.clientX, y: e.clientY};
}else{
explode(explodeOrigin, {x: e.clientX, y: e.clientY});
explodeOrigin = null;
}
});
window['explode'] = explode;
heightIndex = {};
cubesPerX = [];
// Todo fill with gameState.total cubes
cubes = [];
app.ticker.add(delta => gameLoop(delta));
app.stage.addChild(playScene);
}
/*
* UI
*/
function updateUI(){
document.getElementById('total').innerHTML = cubes.length;
}
/*
* Game logic
*/
// Add cube when user clicks
function mine(){
for(let i = 0; i < gameState.cubesPerDig; i++){
setTimeout(addCube, 5*i);
}
}
// Consume a number of cubes
function consumeCubes(nb){
let candidates = _.sampleSize(cubes.filter(c => !c.eDirX), Math.min(nb, cubes.length));
candidates = candidates.slice(0, nb);
candidates.map(c => {
dropCubes(c.x);
c.state = STATE.CONSUMING;
});
}
const STATE = {
LANDING: 0,
CONSUMING: 1,
EXPLODING: 2
}
// Add a cube
function addCube(){
let c = new cube(cubeSize);
let tres = dropSize / cubeSize / 2;
c.x = window.innerWidth / 2 + (_.random(-tres, tres) * cubeSize);
c.y = 0//-cubeSize;
c.speed = _.random(5,8);
cubes.push(c);
c.landed = !1;
c.state = STATE.LANDING;
if(!cubesPerX[c.x]) cubesPerX[c.x] = [];
if (!heightIndex[c.x]) heightIndex[c.x] = window.innerHeight - cubeSize;
cubesPerX[c.x].push(c);
playScene.addChild(c);
}
// Remove a cube
function removeCube(c){
let cube = cubes[c];
playScene.removeChild(cube);
cubes.splice(c,1);
}
// Delete the cube if offscreen
function boundings(c){
let cube = cubes[c];
if(cube.x < 0 || cube.x + cubeSize > window.innerWidth || cube.y < 0 || cube.y > window.innerHeight)
{
removeCube(c);
return true;
}
}
// explode some cubes
function explode(origin, dest){
if(dest.x < origin.x){
dest = [origin, origin = dest][0]; // swap
}
var candidates = cubes.filter(c => c.state != STATE.EXPLODING && c.x >= origin.x && c.x <= dest.x && c.y >= origin.y && c.y <= dest.y);
if(!candidates.length)
return;
for(let i = origin.x; i <= dest.x; i++){
dropCubes(i);
}
candidates.forEach(c => {
c.explodingSpeed = _.random(5,6);
c.eDirX = _.random(-1,1,1) * c.explodingSpeed * c.speed;
c.eDirY = _.random(-1,1,1) * c.explodingSpeed * c.speed;
c.state = STATE.EXPLODING;
});
}
// Drop cubes
function dropCubes(x){
heightIndex[x] = window.innerHeight - cubeSize;
if(cubesPerX[x] && cubesPerX[x].length)
cubesPerX[x].forEach(c => {
if(c.state == STATE.EXPLODING) return;
c.landed = false; c.state = STATE.LANDING;
});
}
/*
* Graphic display
*/
// Cube definition
function cube(size){
let graphic = new PIXI.Graphics();
graphic.beginFill(Math.random() * 0xFFFFFF);
graphic.drawRect(0, 0, size, size);
graphic.endFill();
return graphic;
}
// Init
setup();
})()
/* styles */
/* called by your view template */
* {
box-sizing: border-box;
}
body, html{
margin: 0;
padding: 0;
color:white;
}
#ui{
position: absolute;
z-index: 2;
top: 0;
width: 0;
left: 0;
bottom: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.1/pixi.min.js"></script>
<script>PIXI.utils.skipHello();</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
<!-- Click two points to make cubes explode -->
<div id="ui">
<button id="dig">
Dig 2k cubes
</button>
<button id="consume">
Consume 50 cubes
</button>
<p>
Total : <span id="total"></span>
</p>
</div>
Thanks

Using Sprites will always be faster than using Graphics (at least until v5 comes along!)
So I changed your cube creation function to
function cube(size) {
const sprite = new PIXI.Sprite(PIXI.Texture.WHITE);
sprite.tint = Math.random() * 0xFFFFFF;
sprite.width = sprite.height = size;
return sprite;
}
And that boosted the fps for myself

Related

Freeze scrollbar

I create a game using javascript. The code is placed at the bottom of the page. Visualize it as if the game is below a footer. The game starts when the user selects the start game button. The mechanics of the game are controlled by the arrow key. Arrow keys are used to move up, right, and left down. The problem is when I press the up or down key, the page scrolls too. How to stop this? I want the page not to scroll when the game is active. I am attaching the code of the game I create. It is a snake game.There's CSS too, but I'm not attaching it as I don't think its relevant
<div id="app" class="app">
<div class="start-screen">
<h2>🦊 </h2>
<div class="options">
<h3>Choose Difficulty</h3>
<p class="end-score"></p>
<button data-difficulty="100" class="active">Easy</button>
<button data-difficulty="75">Medium</button>
<button data-difficulty="50">Hard</button>
</div>
<button class="play-btn">Play</button>
</div>
<canvas id="board"></canvas>
<div class="score">0</div>
</div>
<script>
class SnakeGame {
constructor() {
this.$app = document.querySelector('#app');
this.$canvas = this.$app.querySelector('canvas');
this.ctx = this.$canvas.getContext('2d');
this.$startScreen = this.$app.querySelector('.start-screen');
this.$score = this.$app.querySelector('.score');
this.settings = {
canvas: {
width: 500,
height: 500,
background: 'white',
border: 'black'
},
snake: {
size: 20,
background: '#73854A',
border: '#000'
}
};
this.game = {
// "direction" (set in setUpGame())
// "nextDirection" (set in setUpGame())
// "score" (set in setUpGame())
speed: 100,
keyCodes: {
38: 'up',
40: 'down',
39: 'right',
37: 'left'
}
}
this.soundEffects = {
score: new Audio('./sounds/score.mp3'),
gameOver: new Audio('./sounds/game-over.mp3')
};
this.setUpGame();
this.init();
}
init() {
// Choose difficulty
// Rather than using "this.$startScreen.querySelectorAll('button')" and looping over the node list
// and attaching seperate event listeners on each item, it's more efficient to just listen in on the container and run a check at runtime
this.$startScreen.querySelector('.options').addEventListener('click', event => {
this.chooseDifficulty(event.target.dataset.difficulty);
});
// Play
this.$startScreen.querySelector('.play-btn').addEventListener('click', () => {
this.startGame();
});
}
chooseDifficulty(difficulty) {
if(difficulty) {
this.game.speed = difficulty;
this.$startScreen.querySelectorAll('.options button').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
}
}
setUpGame() {
// The snake starts off with 5 pieces
// Each piece is 30x30 pixels
// Each following piece must be n times as far from the first piece
const x = 300;
const y = 300;
this.snake = [
{ x: x, y: y },
{ x: x - this.settings.snake.size, y: y },
{ x: x - (this.settings.snake.size * 2), y: y },
{ x: x - (this.settings.snake.size * 3), y: y },
{ x: x - (this.settings.snake.size * 4), y: y }
];
this.food = {
active: false,
background: '#EC5E0B',
border: '#73AA24',
coordinates: {
x: 0,
y: 0
}
};
this.game.score = 0;
this.game.direction = 'right';
this.game.nextDirection = 'right';
}
startGame() {
// Stop the game over sound effect if a new game was restarted quickly before it could end
this.soundEffects.gameOver.pause();
this.soundEffects.gameOver.currentTime = 0;
// Reset a few things from the prior game
this.$app.classList.add('game-in-progress');
this.$app.classList.remove('game-over');
this.$score.innerText = 0;
this.generateSnake();
this.startGameInterval = setInterval(() => {
if(!this.detectCollision()) {
this.generateSnake();
} else {
this.endGame();
}
}, this.game.speed);
// Change direction
document.addEventListener('keydown', event => {
this.changeDirection(event.keyCode);
});
}
changeDirection(keyCode) {
const validKeyPress = Object.keys(this.game.keyCodes).includes(keyCode.toString()); // Only allow (up|down|left|right)
if(validKeyPress && this.validateDirectionChange(this.game.keyCodes[keyCode], this.game.direction)) {
this.game.nextDirection = this.game.keyCodes[keyCode];
}
}
// When already moving in one direction snake shouldn't be allowed to move in the opposite direction
validateDirectionChange(keyPress, currentDirection) {
return (keyPress === 'left' && currentDirection !== 'right') ||
(keyPress === 'right' && currentDirection !== 'left') ||
(keyPress === 'up' && currentDirection !== 'down') ||
(keyPress === 'down' && currentDirection !== 'up');
}
resetCanvas() {
// Full screen size
this.$canvas.width = this.settings.canvas.width;
this.$canvas.height = this.settings.canvas.height;
this.$canvas.style.border = `3px solid ${this.settings.canvas.border}`;
// Background
this.ctx.fillStyle = this.settings.canvas.background;
this.ctx.fillRect(0, 0, this.$canvas.width, this.$canvas.height);
}
generateSnake() {
let coordinate;
switch(this.game.direction) {
case 'right':
coordinate = {
x: this.snake[0].x + this.settings.snake.size,
y: this.snake[0].y
};
break;
case 'up':
coordinate = {
x: this.snake[0].x,
y: this.snake[0].y - this.settings.snake.size
};
break;
case 'left':
coordinate = {
x: this.snake[0].x - this.settings.snake.size,
y: this.snake[0].y
};
break;
case 'down':
coordinate = {
x: this.snake[0].x,
y: this.snake[0].y + this.settings.snake.size
};
}
// The snake moves by adding a piece to the beginning "this.snake.unshift(coordinate)" and removing the last piece "this.snake.pop()"
// Except when it eats the food in which case there is no need to remove a piece and the added piece will make it grow
this.snake.unshift(coordinate);
this.resetCanvas();
const ateFood = this.snake[0].x === this.food.coordinates.x && this.snake[0].y === this.food.coordinates.y;
if(ateFood) {
this.food.active = false;
this.game.score += 10;
this.$score.innerText = this.game.score;
this.soundEffects.score.play();
} else {
this.snake.pop();
}
this.generateFood();
this.drawSnake();
}
drawSnake() {
const size = this.settings.snake.size;
this.ctx.fillStyle = this.settings.snake.background;
this.ctx.strokestyle = this.settings.snake.border;
// Draw each piece
this.snake.forEach(coordinate => {
this.ctx.fillRect(coordinate.x, coordinate.y, size, size);
this.ctx.strokeRect(coordinate.x, coordinate.y, size, size);
});
this.game.direction = this.game.nextDirection;
}
generateFood() {
// If there is uneaten food on the canvas there's no need to regenerate it
if(this.food.active) {
this.drawFood(this.food.coordinates.x, this.food.coordinates.y);
return;
}
const gridSize = this.settings.snake.size;
const xMax = this.settings.canvas.width - gridSize;
const yMax = this.settings.canvas.height - gridSize;
const x = Math.round((Math.random() * xMax) / gridSize) * gridSize;
const y = Math.round((Math.random() * yMax) / gridSize) * gridSize;
// Make sure the generated coordinates do not conflict with the snake's present location
// If so recall this method recursively to try again
this.snake.forEach(coordinate => {
const foodSnakeConflict = coordinate.x == x && coordinate.y == y;
if(foodSnakeConflict) {
this.generateFood();
} else {
this.drawFood(x, y);
}
});
}
drawFood(x, y) {
const size = this.settings.snake.size;
this.ctx.fillStyle = this.food.background;
this.ctx.strokestyle = this.food.border;
this.ctx.fillRect(x, y, size, size);
this.ctx.strokeRect(x, y, size, size);
this.food.active = true;
this.food.coordinates.x = x;
this.food.coordinates.y = y;
}
detectCollision() {
// Self collison
// It's impossible for the first 3 pieces of the snake to self collide so the loop starts at 4
for(let i = 4; i < this.snake.length; i++) {
const selfCollison = this.snake[i].x === this.snake[0].x && this.snake[i].y === this.snake[0].y;
if(selfCollison) {
return true;
}
}
// Wall collison
const leftCollison = this.snake[0].x < 0;
const topCollison = this.snake[0].y < 0;
const rightCollison = this.snake[0].x > this.$canvas.width - this.settings.snake.size;
const bottomCollison = this.snake[0].y > this.$canvas.height - this.settings.snake.size;
return leftCollison || topCollison || rightCollison || bottomCollison;
}
endGame() {
this.soundEffects.gameOver.play();
clearInterval(this.startGameInterval);
this.$app.classList.remove('game-in-progress');
this.$app.classList.add('game-over');
this.$startScreen.querySelector('.options h3').innerText = 'Game Over';
this.$startScreen.querySelector('.options .end-score').innerText = `Score: ${this.game.score}`;
this.setUpGame();
}
}
const snakeGame = new SnakeGame();
</script>
Stop the page from moving up and down when the game is active
You just need to prevent the default behaviour of these keys:
// Change direction
document.addEventListener('keydown', event => {
event.preventDefault(); // add this line
this.changeDirection(event.keyCode);
});
Note that this isn't a very good idea to do it that way, as it will prevent the default behaviour of all keypresses anywhere on your page. Most notably, F5 will never refresh the browser, and if you have other elements like buttons, links, etc, the standard keypresses to activate those will stop working (turning these off is a particular problem if you want your site to be accessible to those with disabilities).
You'd be better off checking the keyCode first and only preventing default if it corresponds to one of the arrow keys - which isn't so easy to do with your code as it stands but I'd encourage you to rewrite it that way.
Even doing that presents a problem to those who are not able to use a mouse, as (depending on the position of your game interface on the screen and its size) they may still need to scroll down and will be prevented from doing this by this preventDefault behaviour. But well that's what you asked for, and I've given you the solution - but with this important caveat.

I am trying to get a context menu to appear in front of a 3D object

I am trying to make a situation wherein a context menu appears when you click on the mesh of a 3D object. Herein what I have currently:
But what is happening is that the context menu is appearing behind the 3D object and so it is not properly visible. I was wondering what could I do to make the context menu appear in front of the 3D object? The raycast part of my code is herein stated:
public raycast(e: MouseEvent): void {
let mesh;
const x = e.clientX - this.rect.left;
const y = e.clientY - this.rect.top;
let scenePointer = new th.Vector2();
scenePointer.x = (x / this.canvas.clientWidth) * 2 - 1;
scenePointer.y = (y / this.canvas.clientHeight) * - 2 + 1;
var intersect;
// Determine the component in-focus using raycasting
this.raycaster.setFromCamera(scenePointer, this.camera);
const intersects = this.raycaster.intersectObjects(this.scene.children);
var menu = document.getElementById("menu")
var rightclick;
if ((<any>e).which) rightclick = ((<any>e).which == 3);
else if ((<any>e).button) rightclick = ((<any>e).button == 2);
if (!rightclick) {
if (intersects.length > 0) {
this.asmStateServ.focusedComponent = this.assemblyFetchServ.guidToName.get(intersects[0].object.name);
mesh = intersects[0].object
if ((<th.MeshPhongMaterial>mesh.material).color.getHex() != 0x00ff00) {
this.asmStateServ.lastColor = (<th.MeshPhongMaterial>mesh.material).color.getHex();
(<th.MeshPhongMaterial>mesh.material).color = new th.Color(0x00ff00);
}
else {
(<th.MeshPhongMaterial>mesh.material).color = new th.Color(this.asmStateServ.lastColor);
}
}
else {
this.asmStateServ.focusedComponent = '';
}
}
else {
if (intersects.length) {
intersect = intersects[0].object;
menu.style.left = x + "px";
menu.style.top = y + "px";
menu.style.display = "";
}
else {
intersect = undefined;
}
}
}
Any kind of help by modifying my part of the code would be much appreciated. :)

THREE.js - Updating BufferGeometry position twice, causes my FPS to suffer

I have an array of TubeBufferGeometrys that im making animate to look as if they're growing out in length. When the animation runs the first time, it works really smoothly at 60fps. But once i set the geometrys position attributes back to zero, the FPS drop to 30.
Ive isolated my animation to run and then rerun once it finished with only the below changing. Heres the basics of my code:
Animation control view
stop() {
this.play = false;
// Start it again
setTimeout(() => {
let i = this.tubeCount;
while (i--) {
this.tubes[i].lastSegmentSet = 0;
this.tubes[i].updateToPercent(0);
}
this.elapsedTime = 0;
this.startTime = Date.now();
this.play = true;
}, 2000)
}
update() {
requestAnimationFrame(this.animate);
// ..render stuff + composer that ive disabled without effect
if (this.play) {
let percent = (Date.now() - this.startTime) / ANIMATE_DURATION;
if (percent >= 1) {
this.stop();
}
let i = this.lineCount;
while (i--) {
this.tubes[i].updateToPercent(percent);
}
}
}
Tube class (The main animation code)
constructor() {
//..other stuff
this.lastSegmentSet = 0;
}
// I first build the paths, then store the position data to use later to animate to. Then i set all the position data to zero
storeVerticies() {
this.positions = this.tube.geometry.attributes.position.array.slice(0);
const length = this.tube.geometry.attributes.position.array.length;
this.tube.geometry.attributes.position.array = new Float32Array(length);
}
setSegment(segment) {
this.setSegmentTo(segment, segment);
}
setSegmentTo(segment, target) {
let position = this.tube.geometry.attributes.position.array;
let startPoint = segment * JOINT_DATA_LENGTH; //JOINT_DATA_LENGTH is the number of values in the buffer geometry to update a segment
let targetPoint = target * JOINT_DATA_LENGTH;
let n = JOINT_DATA_LENGTH;
while (n--) {
position[startPoint + n] = this.positions[targetPoint + n];
}
}
updateToPercent(percent) {
let endSegment = Math.floor(percent * this.segmentCount);
while (this.lastSegmentSet <= endSegment) {
this.setSegment(this.lastSegmentSet++);
}
let n = this.lastSegmentSet;
while (n <= this.segmentCount + 1) {
this.setSegmentTo(n++, this.lastSegmentSet);
}
this.tube.geometry.attributes.position.needsUpdate = true;
}
Will put bounty when possible

Three.js - How to debug object picking code?

I'm using Three.js on an app, currently I'm having issues with the object picking. Some objects are not getting intersected by the rays, but only on certain camera rotations. I'm trying to debug the code and to draw the ray.
I'm using this methods in my code(canvas is a namespace for the Three objects):
C.getXY = function(e) {
var click = {};
click.x = ( e.clientX / window.innerWidth ) * 2 - 1;
click.y = - ( e.clientY / window.innerHeight ) * 2 + 1;
return click;
};
C.doPicking = function(e) {
var picked = false;
if (canvas.boundingBox !== null) {
canvas.scene.remove(canvas.boundingBox);
}
var projector = new THREE.Projector(), click = C.getXY(e);
var ray = projector.pickingRay(new THREE.Vector3(click.x, click.y, 0), canvas.camera);
ray.linePrecision = 0.00000000000000001;
ray.precision = 0.00000000000000001;
var intersects = ray.intersectObjects(canvas.scene.children);
if (intersects.length > 0) {
var i = 0;
var ids = [];
while (i < intersects.length) {
if (intersects[i].object.visible) {
//Object is picked
}
++i;
}
}
};
My question is...are other points to consider in the debuging process?
Ooops...forgot to add mesh.geometry.computeFaceNormals(); before adding the meshes to the scene. Now the picking is working normally.

PaperJS - How do I connect all items that have a distance of X from any given item? (Item interactivity)

I have a project I am trying to get an animated <canvas> working with Paper JS. What I am curious about is if there is anything built into PaperJS that allows the ability to detect interactivity between items (i.e. if a item is X distance from any other item on the layer). Here is what I have so far:
HTML
<canvas id="myCanvas" resize></canvas>
CSS
html, body{margin:0; padding: 0;}
#myCanvas{width: 100%; height: 100%;}
JS
$(function(){
var canvas = $('#myCanvas')[0];
paper.setup(canvas);
var viewSize = paper.view.size;
var itemCount = 20;
var theBall = new paper.Path.Rectangle({
point : [0,0],
size : 10,
fillColor : '#00a950',
});
var theBallSymbol = new paper.Symbol(theBall);
// Create and place symbol on view
for (var i = 1; i <= itemCount; i++) {
var center = paper.Point.random().multiply(viewSize);
var placedSymbol = theBallSymbol.place(center);
placedSymbol.scale(i / itemCount);
placedSymbol.data = {
origin : center,
direction : (Math.round(Math.random())) ? 'right' : 'left',
}
placedSymbol.onFrame = function(e){
var pathWidth = this.bounds.width * 20;
var center = this.data.origin;
var moveValue = this.bounds.width / 20;
if(this.data.direction == 'right'){
if(this.position.x < center.x + pathWidth){
this.position.x += moveValue;
} else{
this.position.x -= moveValue;
this.data.direction = 'left';
}
} else {
if(this.position.x > center.x - pathWidth){
this.position.x -= moveValue;
} else {
this.position.x += moveValue;
this.data.direction = 'right';
}
}
}
}
paper.view.onFrame = function (e){
// For entire view
for (var i = 0; i < itemCount; i++) {
var item = paper.project.activeLayer.children[i];
// I imagine I would need to do something here
// I tried a hitTest already, but I'm not sure
// that will give me the information I would need
}
}
});
JSFiddle
That part so far is working well. What I am curious about how I can do the following:
Whenever any given item (the squares) come within a distance of X between each other, create a line (path) between them
The idea is very similar to this page: http://panasonic.jp/shaver/lamdash/dna/
Any ideas would be greatly appreciated. Thanks!
Paper.js does not keep track of the inter-point distance between an item's center and all other items. The only way to gather that information is to manually loop through them.
In your case, I think it would be easiest to:
Create an array of lines
Only keep lines that might become shorter than the threshold value
Loop through the lines array on each onFrame() and adjust the opacity.
By only choosing lines that will come within a threshold value, you can avoid creating unnecessary paths that would slow the framerate. Without this, you'd be checking ~5 times as many items.
Here's a quick example:
$(function(){
var canvas = $('#myCanvas')[0];
paper.setup(canvas);
var viewSize = paper.view.size;
var itemCount = 60;
//setup arrays to change line segments
var ballArray = [];
var lineArray = [];
//threshold distance for lines
var threshold = Math.sqrt(paper.view.size.width*paper.view.size.height)/5;
var theBall = new paper.Path.Rectangle({
point : [0,0],
size : 10,
fillColor : '#00a950',
});
var theBallSymbol = new paper.Symbol(theBall);
// Create and place symbol on view
for (var i = 1; i <= itemCount; i++) {
var center = paper.Point.random().multiply(viewSize);
var placedSymbol = theBallSymbol.place(center);
placedSymbol.scale(i / itemCount);
placedSymbol.data = {
origin : center,
direction : (Math.round(Math.random())) ? 'right' : 'left',
}
// Keep each placedSymbol in an array
ballArray.push( placedSymbol );
placedSymbol.onFrame = function(e){
var pathWidth = this.bounds.width * 20;
var center = this.data.origin;
var moveValue = this.bounds.width / 20;
if(this.data.direction == 'right'){
if(this.position.x < center.x + pathWidth){
this.position.x += moveValue;
} else{
this.position.x -= moveValue;
this.data.direction = 'left';
}
} else {
if(this.position.x > center.x - pathWidth){
this.position.x -= moveValue;
} else {
this.position.x += moveValue;
this.data.direction = 'right';
}
}
}
}
// Run through every possible line
// Only keep lines whose length might become less than threshold
for (var i = 0; i < itemCount; i++) {
for (j = i + 1, point1 = ballArray[i].data.origin; j < itemCount; j++) {
if ( Math.abs(point1.y - ballArray[j].bounds.center.y) < threshold && Math.abs(point1.x - ballArray[j].data.origin.x) < 4 * threshold) {
var line = new paper.Path.Line( point1, ballArray[j].bounds.center ) ;
line.strokeColor = 'black';
line.strokeWidth = .5;
//note the index of the line's segments
line.point1 = i;
line.point2 = j;
if (line.length > 1.4 * threshold && ballArray[j].data.direction == ballArray[i].data.direction) {
line.remove();
}
else {
lineArray.push(line);
}
}
}
}
paper.view.onFrame = function (e){
// Update the segments of each line
// Change each line's opacity with respect to distance
for (var i = 0, l = lineArray.length; i < l; i++) {
var line = lineArray[i];
line.segments[0].point = ballArray[line.point1].bounds.center;
line.segments[1].point = ballArray[line.point2].bounds.center;
if(line.length < threshold) {
line.opacity = (threshold - line.length) / threshold;
}
else line.opacity = 0;
}
}
});

Categories

Resources