setTimeout in object method continues to run after object is destroyed - javascript

I have a class called Bullet which is essentially a div on a space invader webpage. When this bullet gets 'fired' I call a method which gradually moves the 'bullet' up the screen.
When the bullet gets to the edge of the screen I want to remove the whole bullet object from memory. However, the setTimeout loop continues to run even after I've deleted it (I think).
I'm sure there is a better way to do this! Perhaps it's foolish to run the loop like this?
TIA
this.bulletmove = new CustomEvent("bulletmove",{detail:this.name});
...
/**
* moves the bullet up the screen gradually
*/
fire(){
var that = this;
setTimeout(function(){
that.moveUp();
window.dispatchEvent(that.bulletmove);
that.fire();
},50);
}
The event is picked up in a controller script which checks if the bullet has reached the edge of the screen at which point it is deleted:
window.addEventListener('bulletmove', function(evt) {
checkCollision(evt);
},false);
...
/**
*Check if the bullet has gone off screen and deletes it
**/
function checkCollision(e){
var bulletName = e.detail;
var bullet = bullets[bulletName];
//check if the bullet has gone off screen
if (bullet.bottom < 0){
bullet.destroy;
delete bullets[e.detail];
bullet=null;
}
}

Have you tried a clearTimeout method to stop the setTimeout from firing?
https://www.freecodecamp.org/news/javascript-settimeout-how-to-set-a-timer-in-javascript-or-sleep-for-n-seconds/
const fireBullet = setTimeout(function(){
that.moveUp();
window.dispatchEvent(that.bulletmove);
that.fire();
},50);
clearTimeout(fireBullet)

I think you should use setInterval instead of calling fire() again - by calling that function, a new setTimeout is created (with a new handler); before the removal of the object, you call obj.halt(), and that clears the setInterval correctly.
const obj = {
name: "objName",
bulletmove() {
return new CustomEvent("bulletmove", {
detail: this.name
})
},
halt() {
clearInterval(this.intervalHandler)
},
intervalHandler: null,
fire() {
const handler = setInterval(() => {
// this.moveUp()
// console.log("move up")
window.dispatchEvent(this.bulletmove())
// this.fire()
}, 500)
this.intervalHandler = handler
},
}
let i = 0
window.addEventListener('bulletmove', function(e) {
// this if-else if mocks the collision detection
// expected: log obj.name 5 times, then clear the interval,
// then event should not be called anymore
if (i < 5) {
console.log(i, e.detail)
} else if (i < 8) {
obj.halt()
console.log(i)
} else if (i < 100) {
console.log(i, e.detail)
}
i++
})
obj.fire()
ANOTHER WAY
A cleaner approach would be if the fire method returned its own "clear function", and you could use that in the event handling:
const obj = {
name: "objName",
bulletmove() {
return new CustomEvent("bulletmove", {
detail: this.name
})
},
fire() {
const handler = setInterval(() => {
// this.moveUp()
// console.log("move up")
window.dispatchEvent(this.bulletmove())
// this.fire()
}, 500)
return () => clearInterval(handler)
},
}
let i = 0
const fireHandler = obj.fire()
const eventHandler = (clearFn) => (e) => {
// this if-else if mocks the collision detection
// expected: log obj.name 5 times, then clear the interval,
// then event should not be called anymore
if (i < 5) {
console.log(i, e.detail)
} else if (i < 8) {
clearFn()
console.log(i)
} else if (i < 100) {
console.log(i, e.detail)
}
i++
}
const eventHandlerWithRemoveFn = eventHandler(fireHandler)
window.addEventListener('bulletmove', eventHandlerWithRemoveFn)
The drawback of this method is that you need to add each object's event handler separately to the window, its benefit is more control, cleaner code (no need to save that handler in the object).
A MODIFIED VERSION FOR MULTIPLE INTERVALS
This is a version of the previous solution, where the clearing functions are stored in the window object:
const eventHandler = (e) => {
const i = e.detail.eventCounter
if (i < 3) {
console.log(i, e.detail.name)
} else if (i < 4) {
window.bulletIntervals[e.detail.name]()
console.log(i, e.detail.name + " is halted")
} else if (i < 100) {
console.log(i, e.detail.name)
}
}
const getBullet = (i) => ({
eventCounter: i, // only for mocking!
name: `objName-${i}`,
bulletmove() {
return new CustomEvent("bulletmove", {
detail: {
name: this.name,
eventCounter: this.eventCounter,
}
})
},
fire() {
const handler = setInterval(() => {
window.dispatchEvent(this.bulletmove())
this.eventCounter++
}, 500)
if (!window.bulletIntervals) window.bulletIntervals = {}
window.bulletIntervals[this.name] = () => clearInterval(handler)
},
})
const bullets = [
getBullet(0),
getBullet(1),
getBullet(2),
]
const fireAll = (bullets) => {
window.addEventListener("bulletmove", eventHandler)
bullets.forEach((bullet) => {
bullet.fire()
})
}
fireAll(bullets)

I would use RxJS to monitor the progress of your bullets.
In the example below I have three different bullets. Each within its own boundary. Once fired, they will immediately stop when they exit their box.
For each bullet we have an "animation frame" observable that emits when such a frame is made available by the browser (internally RxJS uses requestAnimationFrame for this). At that point we check whether the bullet is still within its parent bounding box. If it is we move it otherwise we don't and the subscription to the animation frame stream automatically ends.
const rightPos = el => el.getBoundingClientRect().right;
const moveBullet = (sel, pos) =>
document.querySelector(sel)
.style.left = `${pos}px`;
const fire = (bullet) => {
const el = document.querySelector(bullet);
const parentPos = rightPos(el.parentNode);
return animationFrames().pipe(
map(() => rightPos(el)),
takeWhile(pos => pos < parentPos)
);
}
const bullet1$ = fire('#bullet1');
const bullet2$ = fire('#bullet2');
const bullet3$ = fire('#bullet3');
const fire$ = fromEvent(document.querySelector('button'),'click');
fire$.subscribe(() => {
bullet1$.subscribe(pos => moveBullet('#bullet1', pos+1));
bullet2$.subscribe(pos => moveBullet('#bullet2', pos+1));
bullet3$.subscribe(pos => moveBullet('#bullet3', pos+1));
});
div {
height: 30px;
border: 1px solid black;
margin: 5px;
position: relative;
}
span { position: absolute; }
<script src="https://unpkg.com/rxjs#7.5.7/dist/bundles/rxjs.umd.min.js"></script>
<script>
const {animationFrames, fromEvent} = rxjs;
const {map, takeWhile} = rxjs.operators;
</script>
<div style="width:150px"><span id="bullet1">🏉</span></div>
<div style="width:300px"><span id="bullet2">🥏</span></div>
<div style="width:450px"><span id="bullet3">⚽️</span></div>
<button>Fire!</button>

In the following code, an outer div forms the boundaries of a playfield and all game elements inside the playfield are represented by divs.
Game state consists of an array of game elements (in this instance: one ship and zero or more bullets). Once an element is no longer visible (here: simply off the right-hand side of the playfield), it is removed from game state.
The game loop uses requestAnimationFrame to repeatedly render the game state to the playfield.
Bullet position is calculated using the time of firing and the time elapsed (I added a little randomness to bullet velocity just for fun).
Game elements such as bullets have an associated generator function called as part of the game loop, to retrieve the next state of the element (a bullet "moves by itself" after the initial appearance).
Firing a bullet in this design is as simple as creating a new bullet object with an initial position and an instance of a generator function to account for its trajectory; and then adding that pair to the game state.
const elem = ({ kind = 'div', classN = '' }) => {
const el = document.createElement(kind)
el.classList.add(classN)
return el
}
const applyStyle = (el, style) =>
(Object.entries(style)
.forEach(([k, v]) => el.style[k] = v), el)
const cssPixels = (str) => +(str.slice(0, -2))
const isVisible = (left) =>
cssPixels(left) < cssPixels(playfield.style.width)
const createPlayfield = () =>
applyStyle(elem({ classN: 'playfield' }), { width: '300px' })
const createShip = (startLeft, width) =>
[{ classN: 'ship', style: { left: startLeft, width } }, null]
const createBullet = (startLeft) => {
const b = {
classN: 'bullet',
style: { left: startLeft },
firingTime: +new Date(),
velocity: 0.5,
velocitySeed: Number('1.' + ~~(Math.random() * 9)),
startLeft
}
const g = bulletStateGen(b)
return [ b, () => g.next() ]
}
const bulletPos = ({ firingTime,
startLeft,
velocity,
velocitySeed }, now = +new Date()) =>
`${~~(velocity * (now - firingTime) * velocitySeed + cssPixels(startLeft))}px`
const bulletStateGen = function*(b) {
while (1) {
const left = bulletPos(b)
if (!isVisible(left))
break
b.style = { left }
yield(b)
}
}
const fire = (startLeft) =>
state.unshift(createBullet(startLeft))
const tick = () =>
state = state.reduce((acc, [o, next]) => {
if (!next)
return acc.push([o, next]), acc
const { value, done } = next()
if (done)
return acc
return acc.push([value, next]), acc
}, [])
const blank = () => playfield.innerHTML = ''
const render = () => {
blank()
state.forEach(([{ classN, style = {} }]) =>
playfield.appendChild(applyStyle(elem({ classN }), style)))
}
let ship = createShip('10px', '50px')
let state = [ship]
let playfield = createPlayfield()
const gameLoop = () =>
(render(), tick(), requestAnimationFrame(gameLoop))
const init = () => {
document.body.appendChild(playfield)
document.body.onkeyup = (e) =>
e.key === " "
&& fire(`${cssPixels(ship[0].style.left) + cssPixels(ship[0].style.width)}px`)
}
init()
gameLoop(state, playfield)
.playfield {
height: 300px;
background-color: black;
position: relative;
}
.ship {
top: 138px;
height: 50px;
background-color: gold;
position: absolute;
border-radius: 7px 22px 22px 7px;
}
.bullet {
top: 163px;
width: 10px;
height: 2px;
background-color: silver;
position: absolute;
}
Click on the game to focus it, and then press spacebar to fire!

Related

How to wait for place ship click event to complete before calling next place ship function in JavaScript Battleship game loop

Problem: I call playerPlaceShip (from a DOM interaction module) inside a game loop module. This lets the user hover the board, click to place the ship, and finally in the event handler call board.placeShip() (from the gameBoard module). This works in isolation, but when I add another playerPlaceShip call to place an additional ship, it executes immediately before the first ship can be placed by clicking.
Desired outcome: A way to wait until the click event from the first function call completes before the next function call begins.
What I've tried: Hours of unsuccessfully trying to write and use promises. Hours of reading about promises. Spent a lot of time unsuccessfully trying to rethink how the code is structured. It seems like the click event should be driving the next action, but I don't see how to do that without writing more and more function calls inside the click event handler, which would seem to take control of the game flow away from the game loop module and put it in the DOM interaction module.
Full modules on GitHub: https://github.com/Pete-Fowler/battleship/tree/player-place-ships/src/modules
Code excerpts:
// In game loop module after creating ships, players, and board objects:
// Render Board
renderBoard(p1Board, p1Box);
renderBoard(p2Board, p2Box);
// Player place ships - lets user hover and click board to place
playerPlaceShip(p1Board, p1Carrier);
playerPlaceShip(p1Board, p1Battleship); // this gets called too soon before click event from the first call completes
// In DOM module:
const clickToPlace = (e, board, ship) => {
let { x, y } = e.target.dataset;
x = parseInt(x, 10);
y = parseInt(y, 10)
board.place(ship, x, y, axis);
renderShadow(e, 'place', ship.length);
removeListeners();
}
// Main function for player to place ship
const playerPlaceShip = (board, ship) => {
const squares = document.querySelectorAll('#p1 .board .square');
narrative.textContent = `Lead your ${ship.type} into battle. Press X to steer.`;
squares.forEach(square => {
square.addEventListener('mouseover', (e) => renderShadow(e, 'fill', ship.length));
square.addEventListener('mouseout', (e) => renderShadow(e, 'clear', ship.length));
square.addEventListener('click', (e) => clickToPlace(e, board, ship));
});
window.addEventListener('keydown', (e) => {
if(e.key === 'x') {
switchAxis();
squares.forEach(square => square.classList.remove('hovered'));
renderShadow(lastCoords, 'fill', ship.length);
}
});
}
Thanks!
I wasn't able to checkout the branch, got a strange error: invalid path 'src/images/background.jpg:Zone.Identifier', maybe because of the colon : after jpg. So I downloaded the zip.
Otherwise I would have done a pull request, that would be easier for you to merge.
I added logic so that the ship is always inside the board, and created a custom event to trigger after place ship. There are comments, see if this will help you move on.
game.js
import gameBoard from "./gameBoard";
import player from "./player";
import makeShip from "./ship";
import { p1Box, p2Box, playerPlaceShip, placeShipEventName, AIPlaceShip, renderBoard, UIAttack } from "./DOM";
const startingShipCount = 5;
// SETUP
// Make game boards
const p1Board = gameBoard();
p1Board.init();
const p2Board = gameBoard();
p2Board.init();
// Make players
const p1 = player("Gustav", p1Board, "human");
const p2 = player("Terminator", p2Board, "AI");
// Make p1 ships
const p1Ptb = makeShip("patrolBoat");
const p1Sub = makeShip("sub");
const p1Destroyer = makeShip("destroyer");
const p1Battleship = makeShip("battleship");
const p1Carrier = makeShip("carrier");
// Make AI ships
const p2Ptb = makeShip("patrolBoat");
const p2Sub = makeShip("sub");
const p2Destroyer = makeShip("destroyer");
const p2Battleship = makeShip("battleship");
const p2Carrier = makeShip("carrier");
// Render Board
renderBoard(p1Board, p1Box);
renderBoard(p2Board, p2Box);
// AI place ships
p2Board.place(p2Ptb, 0, 1, "y");
p2Board.place(p2Sub, 2, 6, "y");
p2Board.place(p2Destroyer, 4, 2, "y");
p2Board.place(p2Battleship, 6, 6, "y");
p2Board.place(p2Carrier, 8, 4, "y");
renderBoard(p1Board, p1Box);
renderBoard(p2Board, p2Box);
//################################################
//###################### HANDLE placeShipPhase
//################################################
let countShipsPlaced = 0;
const handlePlaceShipPhase = () => {
countShipsPlaced++;
if (countShipsPlaced == startingShipCount) {
startGame();
} else {
playerPlaceShip(p1Board, p1Carrier);
}
};
//######################################################
//####### LISTENING to the custom event Place Ship
//######################################################
window.addEventListener(placeShipEventName, handlePlaceShipPhase);
// Player places ships
playerPlaceShip(p1Board, p1Carrier);
const startGame = () => {
alert("Game started, battle!");
};
// MAIN GAME LOOP - will need loop
// Player attack
// UIAttack(p2Board);
// AI attack
// Gameover - after exit loop
// The game loop should set up a new game by creating Players and Gameboards.
// For now just populate each Gameboard with predetermined coordinates. You can
// implement a system for allowing players to place their ships later.
// The game loop should step through the game turn by turn using only methods
// from other objects. If at any point you are tempted to write a new function
// inside the game loop, step back and figure out which class or module that
// function should belong to.
// Create conditions so that the game ends once one players ships have all
// been sunk. This function is appropriate for the Game module.
DOM.js
/* eslint-disable no-unused-expressions */
const p1Box = document.querySelector("#p1");
const p2Box = document.querySelector("#p2");
const narrative = document.querySelector("#narrative");
let axis = "y"; // used to render shadow in playerPlaceShip
let selectedSquares = [];
let lastCoords;
const boardSize = 10;
//save the current ship to be used in the "x" key event listender
let currentShip;
//moved outside of the placeship otherwise will add duplicated events
window.addEventListener("keydown", (e) => {
if (e.key.toLocaleLowerCase() === "x") {
const squares = document.querySelectorAll("#p1 .board .square");
switchAxis();
squares.forEach((square) => square.classList.remove("hovered"));
renderShadow(lastCoords, "fill", currentShip.length);
}
});
//#############################################
//##### CREATING the custom event Place Ship
//#############################################
const placeShipEventName = "playerplaceship";
const placeShipEvent = new Event(placeShipEventName);
// Helper functions for playerPlaceShip
const switchAxis = () => {
axis === "x" ? (axis = "y") : (axis = "x");
};
const renderShadow = (e, fill, length) => {
let { x, y } = e.target.dataset;
x = parseInt(x, 10);
y = parseInt(y, 10);
selectedSquares = [];
let count = countOfSquaresOutOfBoard(x, y, length);
//#### LOGIC TO RENDER SHIP ALWAYS INSIDE BOARD
for (let i = -count; i < length - count; i++) {
setSelectedSquares(x, y, i);
}
for (const el of selectedSquares) {
fill === "fill" ? el.classList.add("hovered") : el.classList.remove("hovered");
if (fill === "place") {
el.classList.add("placed");
}
}
lastCoords = e;
};
const removeListeners = () => {
const squares = document.querySelectorAll("#p1 .board .square");
squares.forEach((square) => {
square.replaceWith(square.cloneNode());
});
};
const clickToPlace = (shipSquare, board, ship) => {
let { x, y } = shipSquare.dataset;
x = parseInt(x, 10);
y = parseInt(y, 10);
board.place(ship, x, y, axis);
renderShadow(lastCoords, "place", ship.length);
removeListeners();
//#######################################################
//############# TRIGGERING the custom event place ship
//#########################################################
window.dispatchEvent(placeShipEvent);
console.log(board.getMap());
};
// Main function for player to place ship
const playerPlaceShip = (board, ship) => {
currentShip = ship;
const squares = document.querySelectorAll("#p1 .board .square");
narrative.textContent = `Lead your ${ship.type} into battle. Press X to steer.`;
squares.forEach((square) => {
square.addEventListener("mouseover", (e) => renderShadow(e, "fill", ship.length));
square.addEventListener("mouseout", (e) => renderShadow(e, "clear", ship.length));
square.addEventListener("click", (e) => clickToPlace(selectedSquares[0], board, ship));
});
};
const countOfSquaresOutOfBoard = (x, y, length) => {
let count = 0;
if (axis === "x") {
count = x + length - boardSize;
}
if (axis === "y") {
count = y + length - boardSize;
}
return count < 0 ? 0 : count;
};
const setSelectedSquares = (x, y, i) => {
if (axis === "x") {
selectedSquares.push(document.querySelector(`#p1 .square[data-x="${x + i}"][data-y="${y}"]`));
} else {
selectedSquares.push(document.querySelector(`#p1 .square[data-x="${x}"][data-y="${y + i}"]`));
}
};
// Lets AI place ship
const AIPlaceShip = (board) => {};
const renderBoard = (board, box) => {
// Clear old content prior to re-render if needed
let grid = document.querySelector(`#${box.id} .board`);
if (grid) {
grid.textContent = "";
} else {
grid = document.createElement("div");
grid.className = "board";
}
// Individual squares on board
for (let i = 0; i <= 9; i += 1) {
for (let j = 9; j >= 0; j -= 1) {
const square = document.createElement("div");
square.className = "square";
square.dataset.x = i;
square.dataset.y = j;
grid.append(square);
}
}
box.append(grid);
};
// Player attack phase - sends x, y from clicked square to board.incoming()
const attackCallback = (e, board) => {
const { x, y } = e.target.dataset;
board.incoming(x, y);
const squares = document.querySelectorAll("#p2 .square");
squares.forEach((el) => {
el.removeEventListener("click", attackCallback);
el.classList.remove("hoverable");
});
console.log(board.getMap());
};
// Player attack phase - adds click event listener and hover effect
const UIAttack = (board) => {
const squares = document.querySelectorAll("#p2 .square");
squares.forEach((el) => {
el.addEventListener("click", (e) => attackCallback(e, board));
el.classList.add("hoverable");
});
narrative.textContent = "Click to fire on the enemy fleet";
};
export { p1Box, p2Box, placeShipEventName, playerPlaceShip, AIPlaceShip, renderBoard, UIAttack };

How to scroll while selecting items with mouse in vue

Hi i'm facing the very challenging & interesting problem of scroll during selection of items with mouse drag in both direction i,e up and down
here is a screen shot
Here is my code : https://codesandbox.io/s/select-ivwq8j?file=/src/overridden/Drag-select.vue
Drag-select.vue is the file where drag selection logic is written.
which fires change when files selection gets changed.
I receive those change event here <drag-select-container #change="dragSelect($event)">
Edit 1: after IVO GELO comment
I have added inside drag() function
try{
let containerEl = document.querySelector('#wrapping_container');
let container = containerEl.getBoundingClientRect();
if(box.top > (container.top )){
containerEl.scrollTop = box.top - 50;
return true;
}
}catch(e){
console.log(e);
}
Edit code here: https://codesandbox.io/s/select-ivwq8j?file=/src/overridden/Drag-select.vue
It is very interesting and challenging problem so
Please help me thanks in advance!!
I recommend you use DragSelect js library.
Working Demo
https://codesandbox.io/s/select-forked-tnmnwk?file=/src/components/HelloWorld.vue
mounted() {
const vm = this;
const ds = new DragSelect({
selectables: document.querySelectorAll(".selectable-nodes"),
area: document.getElementById("area"),
draggability: false,
});
ds.subscribe("elementselect", function ({ item }) {
vm.selectedItems.push();
});
ds.subscribe("elementunselect", function ({ item }) {
const index = vm.selectedItems.indexOf(item.getAttribute("customAttribute"));
if (index > -1) {
vm.selectedItems.splice(index, 1);
}
});
}
I found a solution for your question. I rewrite your code in a completely different way. Here is a demo that you can test it. Actually it contains two main component. Parent component that is called "HelloCompo" and its code comes here:
HelloCompo:
<template>
<!-- This is a component that uses "MyDrag" component. -->
<div id="wrapping_container" class="hello">
<!-- Here we insert "MyDrag" component that emits custom "change" event when the selection of element is changed according to user drag. -->
<my-drag #change="dragSelect">
<div
class="item"
:class="{ selected: ( index >= minMax[1] && index <= minMax[0] ) }"
:key="item"
v-for="(item, index) in [1, 2, 3, 4,5,6,7,8,9,10,11,12,13,14,15,16]"
>
{{ item }}
</div>
</my-drag>
</div>
</template>
<script>
import MyDrag from "./MyDrag";
export default {
name: "HelloCompo",
components: {MyDrag},
data() {
return {
selectedItems: [],
};
},
computed: {
minMax: function () {
/* This computed property uses data returned by "MyDrag" component to define the maximum and minimum range to accept "selected" class. */
let max = -1;
let min = -1;
if (this.selectedItems.length > 0) {
max = Math.max(...this.selectedItems);
min = Math.min(...this.selectedItems);
}
return [max-1, min-1]
}
},
methods: {
dragSelect: function (selectedList) {
// console.log(selectedList);
/* this Method is used to set "selectedItems" data after each change in selected drag. */
this.selectedItems = selectedList;
}
},
}
</script>
<style scoped>
.item {
display: block;
width: 230px;
height: 130px;
background: orange;
margin-top: 9px;
line-height: 23px;
color: #fff;
}
.selected {
background: red !important;
}
#wrapping_container{
background:#e7e7e7;
}
</style>
And child component that is called "MyDrag":
MyDrag:
<template>
<section id="parentAll">
<!-- I used "#mousedown" and ... for calling methods instead of using all functions in mounted hook. -->
<div class="minHr" ref="container" #mousedown="startDrag" #mouseup="endDrag" #mousemove="whileDrag">
<slot></slot>
</div>
<!-- This canvas is shown only when the user is dragging on the page. -->
<canvas ref="myCanvas" v-if="showCanvas" #mouseup="endDrag" #mousemove="whileDrag"></canvas>
</section>
</template>
<script>
export default {
name: "MyDrag",
data() {
return {
dragStatus: false, // used for detecting mouse drag
childrenArr: [], // used to store the information of children of 'ref="container"' that comes from "slot"
startEvent: null, // used to detect mouse position on mousedown
endEvent: null, // used to detect mouse position on mouseup
direction: "topBottom", // used to detect the direction of dragging
selectedArr: [], // used to store the selected "divs" after dragging
heightContainer: null, // used to detect the height of 'ref="container"' dynamically
widthContainer: null, // used to detect the width of 'ref="container"' dynamically
/* These data used to draw rectangle on canvas while the user is dragging */
rect: {
startX: null,
startY: null,
w: null,
h: null
},
startDragData: {
x: null,
y: null
},
whileDragData: {
x: null,
y: null,
CLY: null
},
showCanvas: false // used to show or hide <canvas></canvas>
}
},
methods: {
childrenInfo: function () {
/* This method is called on "mounted()" hook to gather information about children of 'ref="container"' that comes from <slot></slot> */
const { container } = this.$refs;
const stylesDiv = window.getComputedStyle(container, null);
this.widthContainer = parseFloat( stylesDiv.getPropertyValue("width") );
this.heightContainer = parseFloat( stylesDiv.getPropertyValue("height") );
let children = container.childNodes;
children.forEach((item, index) => {
let childObj = {
offsetTop: item.offsetParent.offsetTop + item.offsetTop,
offsetHeight: item.offsetHeight
}
this.childrenArr.push(childObj);
})
},
startDrag: function (event) {
/* This method is called at mousedown and detect the click or right click. after that it sets some data like "showCanvas". */
if(event.button === 0) {
this.dragStatus = true;
this.startEvent = event.pageY;
this.startDragData.x = event.pageX;
this.startDragData.y = event.pageY;
this.showCanvas = false;
}
},
whileDrag: async function (event) {
/* This method is called when the user is dragging. Because I want to be confident about showing <canvas> before doing other parts of code, I used "async" function for this method. */
if (this.dragStatus) {
await this.showMethod();
console.log("dragging");
this.whileDragData.x = event.pageX;
this.whileDragData.y = event.pageY;
this.whileDragData.CLY = event.clientY
await this.canvasMethod();
} else {
this.showCanvas = false;
}
},
endDrag: function (event) {
/* This method is called at mouseup. After that it calls other methods to calculate the "divs" that were selected by user. */
if(event.button === 0) {
console.log("end drag");
this.dragStatus = false;
this.showCanvas = false;
this.endEvent = event.pageY;
this.calculateDirection();
this.calculateSelected();
}
},
showMethod: function () {
/* This method is used to set "showCanvas" data at proper time. */
this.showCanvas = true;
},
calculateDirection: function () {
/* This method is used to detect the direction of dragging. */
if (this.startEvent <= this.endEvent) {
this.direction = "topBottom";
} else {
this.direction = "bottomTop";
}
},
calculateSelected: function () {
/* This method is responsible to find out which "divs" were selected while the user was dragging. After that it emits "this.selectedArr" data to the parent component. */
this.selectedArr = [];
let endIndex = null;
let startIndex = null;
this.childrenArr.forEach( (item, index) => {
if ( (item.offsetTop < this.endEvent) && ( (item.offsetTop + item.offsetHeight) > this.endEvent) ) {
endIndex = index;
console.log(endIndex);
}
if ( (item.offsetTop < this.startEvent) && ( (item.offsetTop + item.offsetHeight) > this.startEvent) ) {
startIndex = index;
console.log(startIndex);
}
});
if( endIndex !== null ) {
if (this.direction === "topBottom") {
for (let i = startIndex; i <= endIndex; i++ ) {
this.selectedArr.push(i+1);
}
} else {
for (let i = startIndex; i >= endIndex; i-- ) {
this.selectedArr.push(i+1);
}
}
}
this.$emit("change", this.selectedArr);
},
canvasMethod: function () {
/* This method is used to show a rectangle when user drags on page. It also could understand that the user is near the top or bottom of page, and then it scrolls the page when the user is dragging. */
const { myCanvas } = this.$refs;
myCanvas.width = this.widthContainer;
myCanvas.height = this.heightContainer;
const html = document.documentElement;
let ctx = myCanvas.getContext('2d');
this.rect.startX = this.startDragData.x - myCanvas.offsetParent.offsetLeft;
this.rect.startY = this.startDragData.y - myCanvas.offsetParent.offsetTop;
this.rect.w = (this.whileDragData.x - myCanvas.offsetParent.offsetLeft) - this.rect.startX;
this.rect.h = (this.whileDragData.y - myCanvas.offsetParent.offsetTop) - this.rect.startY ;
if ( Math.abs(this.whileDragData.CLY - window.innerHeight) < 12) {
console.log("near");
html.scrollTop += 25;
}
if ( Math.abs(this.whileDragData.CLY) < 12 ) {
html.scrollTop -= 25;
}
if ( (this.whileDragData.y > (myCanvas.offsetParent.offsetTop + myCanvas.offsetHeight) - 25) || (this.whileDragData.y < myCanvas.offsetParent.offsetTop + 25) ) {
ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
}
ctx.clearRect(0,0,myCanvas.width,myCanvas.height);
ctx.setLineDash([6]);
ctx.strokeRect(this.rect.startX, this.rect.startY, this.rect.w, this.rect.h);
},
},
mounted() {
this.childrenInfo();
}
}
</script>
<style scoped>
.minHr {
min-height: 900px;
}
#parentAll {
position: relative;
}
#parentAll canvas {
position: absolute;
top: 0;
left: 0;
}
</style>
I used a <canvas> to draw rectangle when the user is dragging. The main difference of my code with your is that it shows the selected items after the dragging process was finished. It works in both upward dragging and downward dragging and also when the user is want to continue dragging beyond the window area (scrolling).

there are two object on view both are overlapping each other but event not occurs in phaser 3 javascript

What I want is when the player overlaps with the coin, the coin disappears, but its not for some reason and I don't why the cutcoin function is not called.
function create() {
var healthGroup = this.physics.add.staticGroup({
key: 'ycoin',
frameQuantity: 10,
immovable: true
});
var children = healthGroup.getChildren();
for (var i = 0; i < children.length; i++)
{
var x = Phaser.Math.Between(50, 750);
var y = Phaser.Math.Between(50, 550);
children[i].setPosition(x, y);
}
healthGroup.refresh();
moveCoin = this.add.sprite(60, 340, "ycoin").setInteractive();
this.input.keyboard.on('keydown-A', () => {
moveCoin.allname = "green"
diceNumber = 1
moveCoinStep()
setTimeout(() => {
this.physics.add.overlap(moveCoin,healthGroup,
cutcoin,null,this)
}, 1000);
})
}
function cutcoin(movecoin1, healthGroup) {
console.log('++++++', moveCoin, healthGroup)
}
Don't add this line
this.physics.add.overlap(moveCoin,healthGroup,cutcoin,null,this)
inside keyboard event or on timeout. overlap function is only triggered when overlap occur not on definition. so write the add overlap line outside the keyboard event.
And check the console for any error.

How to correctly wait on state to update/render instead of using a delay/timeout function?

I will attempt to keep this brief, but I am not 100% sure of the correct method of achieving what I am aiming for. I have been thrown in the deep end with React with not much training, so I have most likely been going about most of this component incorrectly, a point in the right direction will definitely help, I don't really expect for someone to completely redo my component for me as it's quite long.
I have a navigation bar SubNav, that finds the currently active item based upon the url/path, this will then move an underline element that inherits the width of the active element. To do this, I find the position of the active item and position accordingly. The same goes for when a user hovers over another navigation item, or when the window resizes it adjusts the position accordingly.
I also have it when at lower resolutions, when the nav gets cut off to have arrows appear to scroll left/right on the navigation to view all navigation items.
Also, if on a lower resolution and the currently active navigation item is off screen, the navigation will scroll to that item and then position the underline correctly.
This, currently works as I have it in my component, this issue is, I don't believe I have done this correctly, I am using a lodash function delay to delay at certain points (I guess to get the correct position of certain navigation items, as it isn't correct at the time of the functions call), which I feel is not the way to go. This is all based on how fast the page loads etc and will not be the same for each user.
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
400,
setArrowStyle(styling)
);
Without using the delay, the values coming back from my state are incorrect as they haven't been set yet.
My question is, how do I go about this correctly? I know my code below is a bit of a read but I have provided a CODESANBOX to play about with.
I have 3 main functions, that all sort of rely on one another:
getPostion()
This function finds the active navigation item, checks if it's within the viewport, if it is not, then it changes the left position of the navigation so it's the leftmost navigation item on the screen, and via setSizes(getSizes()) moves the underline directly underneath.
getSizes()
This is called as an argument within setSizes to update the sizes state, which returns the left and right boundaries of all navigation items
getUnderlineStyle()
This is called as an argument within setUnderLineStyle within the getSizes() function to update the position of the underline object in relation to the position of active navigation item grabbed from the sizes state, but I have to pass the sizesObj as an argument in setSizes as the state has not been set. I think this is where my confusion began, I think I was under the impression, that when I set the state, I could then access it. So, I started using delay to combat.
Below is my whole Component, but can be seen working in CODESANBOX
import React, { useEffect, useState, useRef } from "react";
import _ from "lodash";
import { Link, Route } from "react-router-dom";
import "../../scss/partials/_subnav.scss";
const SubNav = props => {
const subNavLinks = [
{
section: "Link One",
path: "link1"
},
{
section: "Link Two",
path: "link2"
},
{
section: "Link Three",
path: "link3"
},
{
section: "Link Four",
path: "link4"
},
{
section: "Link Five",
path: "link5"
},
{
section: "Link Six",
path: "link6"
},
{
section: "Link Seven",
path: "link7"
},
{
section: "Link Eight",
path: "link8"
}
];
const currentPath =
props.location.pathname === "/"
? "link1"
: props.location.pathname.replace(/\//g, "");
const [useArrows, setUseArrows] = useState(false);
const [rightArrow, updateRightArrow] = useState(false);
const [leftArrow, updateLeftArrow] = useState(false);
const [sizes, setSizes] = useState({});
const [underLineStyle, setUnderLineStyle] = useState({});
const [arrowStyle, setArrowStyle] = useState({});
const [activePath, setActivePath] = useState(currentPath);
const subNavRef = useRef("");
const subNavListRef = useRef("");
const arrowRightRef = useRef("");
const arrowLeftRef = useRef("");
let elsRef = Array.from({ length: subNavLinks.length }, () => useRef(null));
useEffect(
() => {
const reposition = getPosition();
subNavArrows(window.innerWidth);
if (!reposition) {
setSizes(getSizes());
}
window.addEventListener(
"resize",
_.debounce(() => subNavArrows(window.innerWidth))
);
window.addEventListener("resize", () => setSizes(getSizes()));
},
[props]
);
const getPosition = () => {
const activeItem = findActiveItem();
const itemHidden = findItemInView(activeItem);
if (itemHidden) {
const activeItemBounds = elsRef[
activeItem
].current.getBoundingClientRect();
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth =
arrowLeftRef.current !== "" && arrowLeftRef.current !== null
? arrowLeftRef.current.getBoundingClientRect().width
: arrowRightRef.current !== "" && arrowRightRef.current !== null
? arrowRightRef.current.getBoundingClientRect().width
: 30;
const activeItemPos =
activeItemBounds.left * -1 + arrowWidth + currentPos;
const styling = {
left: `${activeItemPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
400,
setArrowStyle(styling)
);
return true;
}
return false;
};
const findActiveItem = () => {
let activeItem;
subNavLinks.map((i, index) => {
const pathname = i.path;
if (pathname === currentPath) {
activeItem = index;
return true;
}
return false;
});
return activeItem;
};
const getSizes = () => {
const rootBounds = subNavRef.current.getBoundingClientRect();
const sizesObj = {};
Object.keys(elsRef).forEach(key => {
const item = subNavLinks[key].path;
const el = elsRef[key];
const bounds = el.current.getBoundingClientRect();
const left = bounds.left - rootBounds.left;
const right = rootBounds.right - bounds.right;
sizesObj[item] = { left, right };
});
setUnderLineStyle(getUnderlineStyle(sizesObj));
return sizesObj;
};
const getUnderlineStyle = (sizesObj, active) => {
sizesObj = sizesObj.length === 0 ? sizes : sizesObj;
active = active ? active : currentPath;
if (active == null || Object.keys(sizesObj).length === 0) {
return { left: "0", right: "100%" };
}
const size = sizesObj[active];
const styling = {
left: `${size.left}px`,
right: `${size.right}px`,
transition: `left 300ms, right 300ms`
};
return styling;
};
const subNavArrows = windowWidth => {
let totalSize = sizeOfList();
_.delay(
() => {
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
300,
setUseArrows(totalSize > windowWidth)
);
};
const sizeOfList = () => {
let totalSize = 0;
Object.keys(elsRef).forEach(key => {
const el = elsRef[key];
const bounds = el.current.getBoundingClientRect();
const width = bounds.width;
totalSize = totalSize + width;
});
return totalSize;
};
const onHover = active => {
setUnderLineStyle(getUnderlineStyle(sizes, active));
setActivePath(active);
};
const onHoverEnd = () => {
setUnderLineStyle(getUnderlineStyle(sizes, currentPath));
setActivePath(currentPath);
};
const scrollRight = () => {
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth = arrowRightRef.current.getBoundingClientRect().width;
const subNavOffsetWidth = subNavRef.current.clientWidth;
let nextElPos;
for (let i = 0; i < elsRef.length; i++) {
const bounds = elsRef[i].current.getBoundingClientRect();
if (bounds.right > subNavOffsetWidth) {
nextElPos = bounds.left * -1 + arrowWidth + currentPos;
break;
}
}
const styling = {
left: `${nextElPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
500,
setArrowStyle(styling)
);
};
const scrollLeft = () => {
const windowWidth = window.innerWidth;
// const lastItemInView = findLastItemInView();
const firstItemInView = findFirstItemInView();
let totalWidth = 0;
const hiddenEls = elsRef
.slice(0)
.reverse()
.filter((el, index) => {
const actualPos = elsRef.length - 1 - index;
if (actualPos >= firstItemInView) return false;
const elWidth = el.current.getBoundingClientRect().width;
const combinedWidth = elWidth + totalWidth;
if (combinedWidth > windowWidth) return false;
totalWidth = combinedWidth;
return true;
});
const targetEl = hiddenEls[hiddenEls.length - 1];
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth = arrowLeftRef.current.getBoundingClientRect().width;
const isFirstEl =
targetEl.current.getBoundingClientRect().left * -1 + currentPos === 0;
const targetElPos = isFirstEl
? targetEl.current.getBoundingClientRect().left * -1 + currentPos
: targetEl.current.getBoundingClientRect().left * -1 +
arrowWidth +
currentPos;
const styling = {
left: `${targetElPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
500,
setArrowStyle(styling)
);
};
const findItemInView = pos => {
const rect = elsRef[pos].current.getBoundingClientRect();
return !(
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
};
const findLastItemInView = () => {
let lastItem;
for (let i = 0; i < elsRef.length; i++) {
const isInView = !findItemInView(i);
if (isInView) {
lastItem = i;
}
}
return lastItem;
};
const findFirstItemInView = () => {
let firstItemInView;
for (let i = 0; i < elsRef.length; i++) {
const isInView = !findItemInView(i);
if (isInView) {
firstItemInView = i;
break;
}
}
return firstItemInView;
};
return (
<div
className={"SubNav" + (useArrows ? " SubNav--scroll" : "")}
ref={subNavRef}
>
<div className="SubNav-content">
<div className="SubNav-menu">
<nav className="SubNav-nav" role="navigation">
<ul ref={subNavListRef} style={arrowStyle}>
{subNavLinks.map((el, i) => (
<Route
key={i}
path="/:section?"
render={() => (
<li
ref={elsRef[i]}
onMouseEnter={() => onHover(el.path)}
onMouseLeave={() => onHoverEnd()}
>
<Link
className={
activePath === el.path
? "SubNav-item SubNav-itemActive"
: "SubNav-item"
}
to={"/" + el.path}
>
{el.section}
</Link>
</li>
)}
/>
))}
</ul>
</nav>
</div>
<div
key={"SubNav-underline"}
className="SubNav-underline"
style={underLineStyle}
/>
</div>
{leftArrow ? (
<div
className="SubNav-arrowLeft"
ref={arrowLeftRef}
onClick={scrollLeft}
/>
) : null}
{rightArrow ? (
<div
className="SubNav-arrowRight"
ref={arrowRightRef}
onClick={scrollRight}
/>
) : null}
</div>
);
};
export default SubNav;
You can make use of useLayoutEffect hook to determine whether the values have been updated and take an action. Since you want to determine whether all the values has been updated, you need to compare old and new values in useEffect. You can refer to the below post to know how to write a usePrevious custom hook
How to compare oldValues and newValues on React Hooks useEffect?
const oldData = usePrevious({ rightArrow, leftArrow, sizes});
useLayoutEffect(() => {
const {rightArrow: oldRightArrow, leftArrow: oldLeftArrow, sizes: oldSizes } = oldData;
if(oldRightArrow !== rightArrow && oldLeftArrow !== leftArrow and oldSizes !== sizes) {
setArrowStyle(styling)
}
}, [rightArrow, leftArrow, sizes])
I think the reason of your delay is necessary here since you calculate based on rectangles of the first and the last element which are affected when you click on button and do animation of scrolling 500ms. So as a result your calculation needs to wait for animation to be done. change the number of animation and delay you will see the relation.
the style I meant.
#include transition(all 500ms ease);
In short, I think what you are using is the right way as long as you have animations related to the calculation.
setState takes an optional second argument which is a callback that executes after the state has been updated and the component has been re-rendered.
Another option is the componentDidUpdate lifecycle method.

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

Categories

Resources