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).
Related
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 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!
I am using the interact.js library for drag-and-dropping elements inside a grid. Once it is inside my grid I want to be able to resize them which is working, but what is the recommend way for preventing two resizable elements from overlapping. I basically want to stop the resizable move listener when it hits another resizable element. I already know when one resizable elements hits another, But how do I prevent the user from being able to overlap them?
full function for resizing elements:
function resizeZone(target, parentContainer, minHeight, width) {
if (interact.isSet(target) == false) {
interact(target)
.resizable({
edges: { top: true, left: false, bottom: true, right: false },
listeners: {
start: function (event) {
startDataSet = event.target.dataset;
startDeltaRect = event.deltaRect;
startRect = event.rect;
},
move: function (event) {
var elementsAtDragLocation = document.elementsFromPoint(event.x0, event.client.y);
for (let elementId in elementsAtDragLocation) {
var result = elementsAtDragLocation[elementId].getAttribute('free-schedule');
var isFalseSet = (result === 'false');
if (isFalseSet) {
let dragLocationRoutineId = elementsAtDragLocation[elementId].id;
let routineId = event.target.id;
// if user drags element past other element they will overlap, because the isFalseSet check won't work =(
if (dragLocationRoutineId !== routineId) {
return;
}
}
}
let { x, y } = event.target.dataset
x = (parseFloat(x) || 0) + event.deltaRect.left
y = (parseFloat(y) || 0) + event.deltaRect.top
Object.assign(event.target.style, {
width: `${event.rect.width}px`,
height: `${event.rect.height}px`,
transform: `translate(${x}px, ${y}px)`
});
Object.assign(event.target.dataset, { x, y });
},
end: async function (event) {
let routineId = event.target.id;
ResizeTaskTrigger(routineId, event.rect.height, event.deltaRect);
},
},
modifiers: [
interact.modifiers.restrictSize({
min: { width: width, height: minHeight },
}),
interact.modifiers.restrictRect({
restriction: parentContainer
}),
],
});
}
}
I am using jointjs to create an interactive flowcharting application, is there a way to prevent elements from being dragged over the top of one another?
You can revert the position of an element when the user finishes dragging and overlap is found.
paper.on({
'element:pointerdown': (elementView, evt) => {
// store the position before the user starts dragging
evt.data = { startPosition: elementView.model.position() };
},
'element:pointerup': (elementView, evt) => {
const { model: element } = elementView;
const { model: graph } = paper;
const elementsUnder = graph.findModelsInArea(element.getBBox()).filter(el => el !== element);
if (elementsUnder.length > 0) {
// an overlap found, revert the position
const { x, y } = evt.data.startPosition;
element.position(x, y);
}
}
});
This is a continuation of another thread.
I have recorded mouse coordinates on the mousemove event with a timestamp, I now want to replay this collection of coordinates and move a mock cursor (img) according to it.
I have tried implementing the solution from the previous thread but I don't know how to practically do it.
This is what needs to happen:
Move img to position
Take time difference from current position to next position
Wait for that amount of time
Move img to next position
I have tried working with setInterval and a timeout function but it doesn't work how its supposed to, try out the code and maybe you can come up with a solution.
Only thing you need to run this is an image to act as the cursor.
import React from 'react'
import cursor from '../../assets/images/cursor.png'
export default class Tracker extends React.Component {
constructor() {
super()
this.state = {
tracking: [],
replay: false,
replayPoint: null,
}
}
onMouseMove = event => {
let newArr = this.state.tracking.slice()
newArr.push({ time: Date.now(), x: event.clientX, y: event.clientY })
this.setState({ tracking: newArr })
}
stopTracking = () => {
window.removeEventListener('mousemove', this.onMouseMove)
this.setState({ replay: true, replayPoint: { x: 0, y: 0 } })
this.state.tracking.forEach(function(p, i, array) {
let that = this
if (i + 1 < array.length) {
let interval = array[i + 1].time - p.time
setTimeout(function() {
that.moveCursor(p)
that.sleep(interval)
}, interval)
}
}, this)
}
moveCursor = p => {
this.setState({
replayPoint: { x: p.x, y: p.y },
})
}
sleep(milliseconds) {
var start = new Date().getTime()
for (var i = 0; i < 1e7; i++) {
if (new Date().getTime() - start > milliseconds) {
break
}
}
}
render() {
let cursor = this.state.replay && <Cursor cords={this.state.replayPoint} />
return (
<div>
{cursor}
<button onClick={this.stopTracking}>Stop tracking</button>
</div>
)
}
componentDidMount() {
window.addEventListener('mousemove', this.onMouseMove)
}
}
class Cursor extends React.PureComponent {
render() {
const { x, y } = this.props.cords
console.log('move to ' + x + ' - ' + y)
return (
<div>
<img
src={cursor}
style={{
left: x + 'px',
top: y + 'px',
position: 'fixed',
width: '20px',
}}
/>
</div>
)
}
}