I am attempting to create a drawing app in JS, however, whenever anything is drawn, it is positioned away from my cursor depending on where it is on the canvas, when I am on the furthest left/bottom side of the canvas, you can draw where your cursor is, but the further right/up I move, the more the brush begins to "drift" and go further than where my cursor is.
const canvas = document.getElementById("canvas");
const increaseBtn = document.getElementById("increase");
const decreaseBtn = document.getElementById("decrease");
const sizeEl = document.getElementById("size");
const colorEl = document.getElementById("color");
const clearEl = document.getElementById("clear");
//Core Drawing Functionality (with some research)
const ctx = canvas.getContext("2d");
let size = 5;
let isPressed = false;
let color = "black";
let x;
let y;
let fakeSize = 1;
canvas.addEventListener("mousedown", (e) => {
isPressed = true;
x = e.offsetX;
y = e.offsetY;
});
canvas.addEventListener("mouseup", (e) => {
isPressed = false;
x = undefined;
y = undefined;
});
canvas.addEventListener("mousemove", (e) => {
if (isPressed) {
const x2 = e.offsetX;
const y2 = e.offsetY;
drawCircle(x2, y2);
drawLine(x, y, x2, y2);
x = x2;
y = y2;
}
});
function drawCircle(x, y) {
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
}
function drawLine(x1, y1, x2, y2) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = color;
ctx.lineWidth = size * 2;
ctx.stroke();
}
function updateSizeOnScreen() {
sizeEl.innerHTML = fakeSize;
}
increaseBtn.addEventListener("click", () => {
size += 5;
fakeSize++;
if (fakeSize > 10) {
fakeSize = 10;
}
if (size > 50) {
size = 50;
}
updateSizeOnScreen();
});
decreaseBtn.addEventListener("click", () => {
size -= 5;
fakeSize--;
if (fakeSize < 1) {
fakeSize = 1;
}
if (size < 5) {
size = 5;
}
updateSizeOnScreen();
});
colorEl.addEventListener("change", (e) => {
color = e.target.value;
});
clearEl.addEventListener("click", () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
//Eraser and Pencil Actions (my own algorithm)
const eraser = document.getElementById("eraser");
const pencil = document.getElementById("pencil");
eraser.addEventListener("click", () => {
localStorage.setItem("colorEl", JSON.stringify(color));
color = "#fff";
colorEl.disabled = true;
canvas.classList.add("eraseractive");
eraser.classList.add("eraseractive");
colorEl.classList.add("eraseractive");
canvas.classList.remove("pencilactive");
eraser.classList.remove("pencilactive");
colorEl.classList.remove("pencilactive");
});
pencil.addEventListener("click", () => {
JSON.parse(localStorage.getItem("colorEl"));
color = colorEl.value;
colorEl.disabled = false;
canvas.classList.remove("eraseractive");
eraser.classList.remove("eraseractive");
colorEl.classList.remove("eraseractive");
canvas.classList.add("pencilactive");
eraser.classList.add("pencilactive");
colorEl.classList.add("pencilactive");
});
// Dark/Light Mode
const darkMode = document.getElementById("darkMode");
const lightMode = document.getElementById("lightMode");
const toolbox = document.getElementById("toolbox");
darkMode.addEventListener("click", () => {
darkMode.classList.add("mode-active");
lightMode.classList.remove("mode-active");
lightMode.classList.add("rotate");
darkMode.classList.remove("rotate");
toolbox.style.backgroundColor = "#293462";
document.body.style.backgroundImage =
"url('/assets/images/darkModeBackground.svg')";
document.body.style.backgroundSize = "1920px 1080px";
canvas.style.borderColor = "#293462";
toolbox.style.borderColor = "#293462";
});
lightMode.addEventListener("click", () => {
lightMode.classList.add("mode-active");
darkMode.classList.remove("mode-active");
darkMode.classList.add("rotate");
lightMode.classList.remove("rotate");
toolbox.style.backgroundColor = "#293462";
document.body.style.backgroundImage =
"url('/assets/images/lightModeBackground.svg')";
document.body.style.backgroundSize = "1920px 1080px";
canvas.style.borderColor = "#293462";
toolbox.style.borderColor = "#293462";
});
* {
box-sizing: border-box;
font-size: 20px !important;
}
body {
background: url("https://drawing-app-green.vercel.app/assets/images/lightModeBackground.svg");
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
margin: 0;
position: relative;
max-height: 100vh;
overflow: hidden;
}
::selection {
background: transparent;
}
::-moz-selection {
background: transparent;
}
.mode {
display: flex;
position: absolute;
top: 10px;
right: 25px;
cursor: pointer;
}
.light-mode {
color: yellow;
}
.dark-mode {
color: #16213e;
}
.container {
display: flex;
flex-direction: column;
max-width: 1200px;
width: 100%;
max-height: 600px;
height: 100%;
}
canvas {
display: flex;
border: 2px solid #293462;
cursor: url("https://drawing-app-green.vercel.app/assets/images/pencilCursor.png") 2 48, pointer;
background-color: #fff;
margin-top: 3rem;
width: 100%;
height: 600px;
}
.toolbox {
background-color: #293462;
border: 1px solid #293462;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
padding: 0.2rem;
}
.toolbox > * {
background-color: #fff;
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 2rem;
height: 30px;
width: 30px;
margin: 0.25rem;
padding: 0.25rem;
cursor: pointer;
}
.toolbox > *:last-child {
margin-left: auto;
}
canvas.eraseractive {
cursor: url("https://drawing-app-green.vercel.app/assets/images/eraserCursor.png") 2 48, pointer;
}
#color.eraseractive {
cursor: not-allowed;
}
canvas.pencilactive {
cursor: url("https://drawing-app-green.vercel.app/assets/images/pencilCursor.png") 2 48, pointer;
}
.mode-active {
visibility: hidden;
}
.rotate {
transform: rotate(360deg);
transition: transform 1s linear;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drawing App</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
</head>
<body>
<i class="fa-solid fa-moon dark-mode fa-2x mode" id="darkMode"></i>
<i
class="fa-solid fa-sun light-mode fa-2x mode mode-active"
id="lightMode"
></i>
<div class="container">
<canvas id="canvas" width="1024" height="600"></canvas>
<div class="toolbox" id="toolbox">
<button id="decrease">-</button>
<span id="size">1</span>
<button id="increase">+</button>
<input type="color" id="color" />
<button id="pencil">
<img src="assets/images/pencilCursor.png" alt="" />
</button>
<button id="eraser">
<img src="assets/images/eraserCursor.png" alt="" />
</button>
<button id="clear">X</button>
</div>
</div>
<script src="assets/js/script.js"></script>
</body>
</html>
Your problem is that your canvas dimentions don't match with the dimentions of the HTML element that contains it. You see: your canvas has a fixed width="" and height="" attributes set. But in your HTML your canvas element has a width of 100%. So that means that the container vairies in dimentions but the canvas inside it not. This result in the canvas trying to resize to show inside the container thus giving you issues with calculating exacly what pixel you are clicking.
You have two options:
Option 1: calculate your click position taking into account canvas deformation
If you want your canvas to resize, then calculate the real position using a simple ratio formula. If for example your canvas has a width of 100 but right now its container is 10px wide, then if you click on pixel 5 you expect a dot to be drawn at pixel 50. In other words if your canvas is smaller by a factor of 10 then you need to multiply your position by a factor of 10.
In your code it would look something like this:
// this is your same code in lines 33 ana34 but see that I added a multiplication by the ratio between the canvas size and the canvas container
const x2 = e.offsetX * (canvas.width / ctx.canvas.getBoundingClientRect().width);
const y2 = e.offsetY * (canvas.height / ctx.canvas.getBoundingClientRect().height);
Option #2: Dont allow your canvas to deform
Remove the container class, and remove the width:100% from your canvas css. Your canvas will overflow and cause a scrollbar but the positions will be calculated properly with your code.
I have some Javascript drawing random square elements in the DOM. I have a gif (Image) I want these elements to appear over but they keep appearing underneath the gif. I tried defining z-depth and layout parameters to move these elements on top of the image here, but this produced no difference.
Any assistance in achieving the result (drawing elements onclick, on top of this gif) would be much appreciated.
I ultimately want to draw various other images over this image onclick, restricted to this particular area on top of the gif. If someone can suggest a solution to this as well I would be very much grateful!
(Code features some unused elements from my past attempts)
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="div.css" />
</head>
<body>
<div style="cursor: pointer;" id="boxy" >
<img src="bg.gif" alt="unfinished bingo card" onclick="create()" />
</div>
</div>
<script>
var body = document.getElementsByTagName("body")[0];
var canvas = document.createElement("canvas");
canvas.height = 1300;
canvas.width = 1300;
var context = canvas.getContext("2d");
body.appendChild(canvas);
var rects = [];
function create() {
// Opacity
context.globalAlpha = 0.7;
var color = '#' + Math.round(0xffffff * Math.random()).toString(16);
context.fillStyle = color;
//Each rectangle's size is (20 ~ 100, 20 ~ 100)
var coordx = Math.random() * canvas.width;
var coordy = Math.random() * canvas.width;
var width = Math.random() * 80 + 20;
var height = Math.random() * 80 + 20;
var rect = {
x: coordx,
y: coordy,
w: width,
h: height
}
var ok = true;
rects.forEach(function (item) {
if (isCollide(rect, item)) {
console.log("collide");
ok = false;
} else {
console.log("no collision");
}
})
if (ok) {
context.fillRect(coordx, coordy, width, height);
rects.push(rect);
} else {
console.log('rect dropped');
}
console.log(rects);
}
function isCollide(a, b) {
return !(
((a.y + a.h) < (b.y)) ||
(a.y > (b.y + b.h)) ||
((a.x + a.w) < b.x) ||
(a.x > (b.x + b.w))
);
}
document.getElementById('boxy').addEventListener('click', create);
document.getElementById('canvas').style.position = "relative";
document.getElementById('canvas').style.zIndex = "10";
</script>
</body>
</html>
#my-div {
width: 1300x;
height: 1300px;
z-index: -1;
}
a.fill-div {
display: block;
height: 100%;
width: 100%;
text-decoration: none;
}
#boxy {
display: inline-block;
height: 100%;
width: 100%;
text-decoration: none;
z-index: -1;
}
.canvas {
display: inline-block;
height: 100%;
width: 100%;
text-decoration: none;
z-index: 10;
}
You have to use position:absolute; to take it out of the html flow.
Now anything added after the image will be placed like the image was never there.
img {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: -10;
}
div {
font-size: 2rem;
color: white;
}
<img src="https://images.unsplash.com/photo-1664273107076-b6d1fbfb973b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1171&q=80">
<div>Hello i am on top of the image
</div>
I am trying to add circles when the event mousedown is generated into the square-one (grey square) only. If the mouse hovers outside of the square-one, it should not insert any circles anywhere else such as square-two(green square).
Question: How can I set the limits for the circles such that they are only inserted within the square-one boundaries? Thank you for your help.
***********
JavaScript
***********
let count = 1
let greySquare = document.getElementById("square-one")
let mousePosition;
let circlesArray = []
document.addEventListener('mousedown', (event)=>{
let circle = document.createElement('div')
let circleHeight = 40
let circleWidth = 40
mousePosition = {
x: event.clientX,
y: event.clientY
}
circle.style.height = `${circleHeight}px`
circle.style.width = `${circleWidth}px`;
circle.style.borderRadius = "50%"
circle.style.backgroundColor = `#F0B27A`
circle.style.position = "absolute"
circle.style.left = (mousePosition.x - (circleWidth/2)) + "px"
circle.style.top = (mousePosition.y - (circleHeight/2)) + "px"
circle.style.lineHeight = `${circleHeight}px`
circle.style.display = 'flex';
circle.style.cursor = 'pointer'
circle.style.justifyContent = 'center';
circle.style.border = 'none'
circle.textContent = count++
greySquare.appendChild(circle)
circlesArray.push(circle)
})
********
HTML
********
<body>
<div class="container">
<div id="square-one"></div>
<div id="square-two"></div>
</div>
<script src="script.js"></script>
</body>
******
CSS
******
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
body{
position: relative;
}
.container{
position: relative;
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#square-one{
position: relative;
width: 300px;
height: 300px;
background-color: grey;
margin-right: 100px;
}
#square-two{
position: relative;
width: 300px;
height: 300px;
background-color: green;
}
When I use your code, the circles aren't being put where I actually click.
That is because you're using the mouse's position (which is relative to the page) to detect where you will put the circles, but then you append them to the
graySquare, which doesn't start at (0,0). If you append them to the .contaner instead, you'll be ok.
document.querySelector(".container").appendChild(circle)
Then regarding the set the limits for the circles such that they are only inserted within the square-one boundaries, you need to get the position (x and y), width and height of the squareOne and only proceed if the click occurs within those.
document.addEventListener('mousedown', (event)=>{
mousePosition = {
x: event.clientX,
y: event.clientY
}
let greySquarePosition = greySquare.getBoundingClientRect();
if(!(mousePosition.x>=greySquarePosition.left + window.scrollX&&
mousePosition.x<=greySquarePosition.left + window.scrollX + greySquarePosition.width&&
mousePosition.y>=greySquarePosition.top + window.scrollY&&
mousePosition.y<=greySquarePosition.top + window.scrollY + greySquarePosition.height))
return;
// ...
I used this to get the position of the div, and this to get its width and height (although they ended up being the same solution).
EDIT!
I kept thinking about this and there's a more obvious, more elegant solution (to me at least). You add the event listener to the graySquare and not the whole document.
greySquare.addEventListener('mousedown', (event)=> ...
Then you don't need the ugly part where you check if the mouse is within the limits.
You can even bind the same function to different squares. Check the updated snippet.
let count = 1
let greySquare = document.getElementById("square-one")
let greenSquare = document.getElementById("square-two")
let mousePosition;
let circlesArray = []
greySquare.addEventListener('mousedown', paintCircles.bind(null, '#F0B27A'), false);
greenSquare.addEventListener('mousedown', paintCircles.bind(null, '#fa0123'), false);
function paintCircles(color, event){
mousePosition = {
x: event.clientX,
y: event.clientY
}
let circle = document.createElement('div')
let circleHeight = 40
let circleWidth = 40
circle.style.height = `${circleHeight}px`
circle.style.width = `${circleWidth}px`;
circle.style.borderRadius = "50%"
circle.style.backgroundColor = `${color}`
circle.style.position = "absolute"
circle.style.left = (mousePosition.x - (circleWidth/2)) + "px"
circle.style.top = (mousePosition.y - (circleHeight/2)) + "px"
circle.style.lineHeight = `${circleHeight}px`
circle.style.display = 'flex';
circle.style.cursor = 'pointer'
circle.style.justifyContent = 'center';
circle.style.border = 'none'
circle.textContent = count++;
document.querySelector(".container").appendChild(circle)
circlesArray.push(circle)
}
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
body{
position: relative;
}
.container{
position: relative;
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#square-one{
position: relative;
width: 300px;
height: 300px;
background-color: grey;
margin-right: 100px;
}
#square-two{
position: relative;
width: 300px;
height: 300px;
background-color: green;
}
<body>
<div class="container">
<div id="square-one"></div>
<div id="square-two"></div>
</div>
</body>
I am writing my own Drag'n'drop functionality for one of my projects and I am running into an issue. All my "draggable" elements are inside a container with display:flex. On mousedown event on one of this elements I set the position to absolute so I would be able to set the left and top properties of the element when I am dragging. Here is what I am doing:
let container = document.querySelector("#big-container")
var dragging = false;
var draggedObject;
let shiftX=0;
let shiftY=0;
document.querySelectorAll(".draggable").forEach((draggable,index) => {
draggable.style.order = index;
draggable.draggable =false;
draggable.ondragstart = ()=>{return false}
draggable.addEventListener("mousedown",ev =>{
draggedObject = draggable;
shiftX = ev.offsetX+5;
shiftY = ev.offsetY+5;
draggable.style.position = "absolute";
draggable.style.left = (ev.clientX - shiftX) + 'px';
draggable.style.top = (ev.clientY - shiftY) + 'px';
dragging = true;
let placeholder = document.createElement("div");
placeholder.id = "placeholder";
placeholder.style.order = draggable.style.order;
container.appendChild(placeholder);
})
})
document.addEventListener("mousemove", ev =>{
if(dragging){
draggedObject.style.left = ev.clientX - shiftX + 'px';
draggedObject.style.top = ev.clientY - shiftY + 'px';
}
})
document.addEventListener("mouseup",ev =>{
if(dragging){
draggedObject.style.position = 'static'
let placeholder = document.querySelector("#placeholder");
container.removeChild(placeholder);
dragging = false
}
})
/* the :not(:last-of-type(div)) is there so the console doesn't get affected */
*{
margin: 0;
padding: 0;
box-sizing: border-box;
background-color: black;
}
.draggable {
width: 90px;
height: 120px;
margin: 5px;
}
#placeholder {
width: 90px;
height: 120px;
margin: 5px;
background-color: rgba(0, 0, 0, 0.3);
border: dashed grey 5px;
}
<body draggable="false" ondragstart="return false;">
<div id = "big-container" style ="display: flex; background-color: rgb(76, 104, 95); width: 500px; height: 500px;">
<div style="background-color: rgb(204, 125, 111);" class="draggable"></div>
<div style="background-color: rgb(170, 214, 120);" class="draggable"></div>
<div style="background-color: rgb(129, 212, 167);" class="draggable"></div>
<div style="background-color: rgb(162, 137, 196);" class="draggable"></div>
</div>
</body>
What I am trying to achieve is that on mousedown the element should stay where it was and after that when I move my mouse to move the element as well.(the anchor point should be where I clicked the element)
I am doing shiftX = ev.offsetX+5; because I need to account for the element's margin.
The issue is when I click on a element(and don't move my mouse at all), you can see a little shift in the element's position. It is very minor(maybe 1 or 2px) and is not happening in all places(some zones in the element do not introduce this position shift)
Do you guys have any idea what might be causing it?
You can use getBoundingClientRect() to get the actual position.
let container = document.querySelector("#big-container");
var dragging = false;
var draggedObject;
let shiftX = 0;
let shiftY = 0;
document.querySelectorAll(".draggable").forEach((draggable, index) => {
draggable.style.order = index;
draggable.draggable = false;
draggable.ondragstart = () => {
return false;
};
draggable.addEventListener("mousedown", (ev) => {
draggedObject = draggable;
var x = draggable.getBoundingClientRect().top - 5;
var y = draggable.getBoundingClientRect().left - 5;
shiftX = ev.offsetX + 5;
shiftY = ev.offsetY + 5;
draggable.style.position = "absolute";
draggable.style.left = y + "px";
draggable.style.top = x + "px";
dragging = true;
let placeholder = document.createElement("div");
placeholder.id = "placeholder";
placeholder.style.order = draggable.style.order;
container.appendChild(placeholder);
});
});
document.addEventListener("mousemove", (ev) => {
if (dragging) {
draggedObject.style.left = ev.clientX - shiftX + "px";
draggedObject.style.top = ev.clientY - shiftY + "px";
}
});
document.addEventListener("mouseup", (ev) => {
if (dragging) {
draggedObject.style.position = "static";
let placeholder = document.querySelector("#placeholder");
container.removeChild(placeholder);
dragging = false;
}
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
background-color: black;
}
#big-container {
width: 500px;
height: 500px;
}
.draggable {
width: 90px;
height: 120px;
margin: 5px;
}
#placeholder {
width: 90px;
height: 120px;
margin: 5px;
background-color: rgba(0, 0, 0, 0.3);
border: dashed grey 5px;
}
<body draggable="false" ondragstart="return false;">
<div
id="big-container"
style="display: flex; background-color: rgb(76, 104, 95);"
>
<div
style="background-color: rgb(204, 125, 111);"
class="draggable"
></div>
<div
style="background-color: rgb(170, 214, 120);"
class="draggable"
></div>
<div
style="background-color: rgb(129, 212, 167);"
class="draggable"
></div>
<div
style="background-color: rgb(162, 137, 196);"
class="draggable"
></div>
</div>
</body>
On mobile, it's a common UI pattern to have a scrollable element inside a draggable element. When you reach the end of the scrollable element, you start dragging the outer element. E.g. in this GIF (https://media.giphy.com/media/9MJgBkoZfqA7jRdQop/giphy.gif), after scrolling to the top, if you continuing scrolling, it'll drag the subreddits menu.
I want to implement a similar pattern using JS/CSS. To do this, I need to detect if users continue scrolling after reaching the end. Is this possible? If so, is it possible to determine how much they scroll after reaching the end?
window.onscroll = function(element) {
if ((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight) {
alert("you're at the bottom of the page");
}
};
Using element parameter to know the current exact x y where mouse is now at to calculate more and some how much was scrolled
Javascript: How to detect if browser window is scrolled to bottom?
If You need to keep track of the user activity after the bottom (or the top) of the page has been reached, beside the scroll event, You need to track the the wheel event. Moreover, on mobile, You need to track also touchstart and touchmove events.
Not all these events are normalized across browsers, so I did my own normalization function, which is more or less something like this:
var compulsivity = Math.log2(Math.max(scrollAmount, 0.01) * wheelAmount);
Below is a complete playground. You can test it in Chrome using the Mobile View of the Developer Tools, or in other browsers using the TouchEmulator.
function Tracker(page) {
this.page = page;
this.moveUp = 0;
this.moveDown = 0;
this.startTouches = {};
this.moveTouches = {};
this.lastScrollY = 0;
this.monitor = {};
this.startThreshold = 160;
this.moveThreshold = 10;
this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
this.pullToRefresh = window.chrome || navigator.userAgent.match('CriOS');
this.amplitude = 16 / Math.log(2);
this.page.ownerDocument.addEventListener( 'onwheel' in document ? 'wheel' : 'onmousewheel' in document ? 'mousewheel' : 'DOMMouseScroll', this, { passive: true } );
/* The basic scroll event cannot be canceled, so it does not need to be set passive.*/
this.page.ownerDocument.addEventListener('scroll', this);
this.page.addEventListener('touchstart', this, { passive: true });
/* Maybe we need to cancel pullToRefresh */
this.page.addEventListener('touchmove', this, { passive: false });
return this;
}
Tracker.prototype.handleEvent = function (e) { /* handleEvent is built-in */
var winHeight = (this.iOS ? document.documentElement.clientHeight : window.innerHeight) | 0,
currScrollY = window.pageYOffset | 0,
amountScrollY = (this.lastScrollY - currScrollY) | 0,
elHeight = this.page.offsetHeight | 0,
elTop = -currScrollY, elBottom = winHeight - elHeight + currScrollY,
isTop = elTop >= 0, isBottom = elBottom >= 0;
switch (e.type) {
case 'wheel':
case 'onmousewheel':
case 'mousewheel':
case 'DOMMouseScroll':
var wheelDelta = e.wheelDelta ? e.wheelDelta : e.deltaY ? -e.deltaY : -e.detail,
wheelDir = (wheelDelta > 0) - (wheelDelta < 0),
wheelUp = wheelDir < 0, wheelDown = wheelDir > 0,
wheelAmount = 100 * wheelDir;
if (isTop && wheelDown) {
this.moveUp++;
this.moveDown = 0;
} else if (isBottom && wheelUp) {
this.moveUp = 0;
this.moveDown++;
} else {
this.moveUp = 0;
this.moveDown = 0;
}
var compulsivity = this.amplitude * Math.log(Math.max(this.moveUp, this.moveDown, 0.01) * wheelAmount* wheelDir);
this.monitor[e.type].track(wheelAmount, compulsivity);
break;
case 'scroll':
/* end of scroll event for iOS, start/end of scroll event for other browsers */
this.lastScrollY = currScrollY;
this.monitor[e.type].track(amountScrollY, 0);
break;
case 'touchstart':
var touches = [].slice.call(e.touches), i = touches.length;
while (i--) {
var touch = touches[i], id = touch.identifier;
this.startTouches[id] = touch;
this.moveTouches[id] = touch;
}
break;
case 'touchmove':
var touches = [].slice.call(e.touches), i = touches.length,
currTouches = {},
swipeUp = false, swipeDown = false,
currMoveY = 0, totalMoveY = 0;
while (i--) {
var touch = touches[i], id = touch.identifier;
currTouches[id] = touch;
if (id in this.moveTouches) {
currMoveY = this.moveTouches[id].screenY - touch.screenY;
}
if (id in this.startTouches) {
totalMoveY = this.startTouches[id].screenY - touch.screenY;
}
swipeUp = currMoveY > 0 || totalMoveY > 0;
swipeDown = currMoveY < 0 || totalMoveY < 0;
if (this.pullToRefresh && isTop && swipeDown && e.cancelable) {
e.preventDefault();
console.log('Reload prevented');
}
}
this.moveTouches = currTouches;
var moveDir = (totalMoveY > 0) - (totalMoveY < 0),
longSwipe = moveDir * totalMoveY > this.startThreshold,
shortSwipe = moveDir * totalMoveY > this.moveThreshold,
realSwipe = longSwipe || shortSwipe;
if (isTop && swipeDown) {
if (realSwipe) this.moveUp++;
this.moveDown = 0;
} else if (isBottom && swipeUp) {
this.moveUp = 0;
if (realSwipe) this.moveDown++;
} else {
this.moveUp = 0;
this.moveDown = 0;
}
var compulsivity = this.amplitude * Math.log(Math.max(this.moveUp, this.moveDown, 0.01) * moveDir * totalMoveY);
this.monitor[e.type].track(currMoveY, compulsivity);
break;
}
};
function Monitor(events) {
this.ctx = null;
this.cont = null;
this.events = events;
this.values = [];
this.average = 0;
this.lastDrawTime = 0;
this.inertiaDuration = 200;
return this;
}
Monitor.prototype.showOn = function (container) {
var cv = document.createElement('canvas');
this.ctx = cv.getContext('2d');
this.cont = document.getElementById(container);
cv.width = this.cont.offsetWidth;
cv.height = this.cont.offsetHeight;
cv.style.top = 0;
cv.style.left = 0;
cv.style.zIndex = -1;
cv.style.position = 'absolute';
cv.style.backgroundColor = '#000';
this.cont.appendChild(cv);
var self = this;
window.addEventListener('resize', function () {
var cv = self.ctx.canvas, cont = self.cont;
cv.width = cont.offsetWidth;
cv.height = cont.offsetHeight;
});
return this;
};
Monitor.prototype.track = function (value, average) {
this.average = average;
if (this.values.push(value) > this.ctx.canvas.width) this.values.shift();
if (value) this.lastDrawTime = new Date().getTime();
};
Monitor.prototype.draw = function () {
if (this.ctx) {
var cv = this.ctx.canvas, w = cv.width, h = cv.height;
var i = this.values.length, x = w | 0, y = (0.5 * h) | 0;
cv.style.backgroundColor = 'rgb(' + this.average + ', 0, 0)';
this.ctx.clearRect(0, 0, w, h);
this.ctx.strokeStyle = '#00ffff';
this.ctx.lineWidth = 1;
this.ctx.beginPath();
while (i--) {
x -= 4;
if (x < 0) break;
this.ctx.moveTo(x, y);
this.ctx.lineTo(x + 1, y);
this.ctx.lineTo(x + 1, y - this.values[i]);
}
this.ctx.stroke();
var elapsed = new Date().getTime() - this.lastDrawTime;
/* cool down */
this.average = this.average > 0 ? (this.average * 0.9) | 0 : 0;
if (elapsed > this.inertiaDuration) {
this.track(0, this.average);
}
}
var self = this;
setTimeout(function () {
self.draw();
}, 100);
};
Monitor.prototype.connectTo = function (tracker) {
var events = this.events.split(' '), i = events.length;
while (i--) {
tracker.monitor[events[i]] = this;
}
this.draw();
return this;
};
function loadSomeData(target) {
$.ajax({
url: 'https://jsonplaceholder.typicode.com/users',
method: 'GET',
crossDomain: true,
dataType: 'json',
success: function (users) {
var html = '', $ul = $(target).find('ul');
$.each(users, function (i, user) {
var item = '<li><a class="ui-alt-icon ui-nodisc-icon">';
item += '<h2>' + user.name + '</h2>';
item += '<p><strong>' + user.company.name + '</strong></p>';
item += '<p>' + user.address.zipcode + ', ' + user.address.city + '</p>';
item += '<p>' + user.phone + '</p>';
item += '<p>' + user.email + '</p>';
item += '<p class="ui-body-inherit ui-li-aside ui-li-count"><strong>' + user.id + '</strong></p>';
item += '</a></li>';
html += item;
});
$ul.append(html).listview('refresh');
},
});
}
$(document)
.on('pagecreate', '#page-list', function (e) {
$("[data-role='header'], [data-role='footer']").toolbar({ theme: 'a', position: 'fixed', tapToggle: false });
loadSomeData(e.target);
})
.on('pageshow', '#page-list', function (e, ui) {
var tracker = $.data(this, 'mobile-page', new Tracker(this));
new Monitor('touchstart touchmove').connectTo(tracker).showOn('header');
new Monitor('scroll wheel mousewheel DOMMouseScroll').connectTo(tracker).showOn('footer');
});
.ui-page {
touch-action: none;
}
h1, h2, h3, h4, h5, h6, p {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* JQM no frills */
.ui-btn,
.ui-title,
.ui-btn:hover,
.ui-btn:focus,
.ui-btn:active,
.ui-btn:visited {
text-shadow: none !important;
}
* {
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Compulsivity</title>
<meta name="description" content="Compulsivity" />
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, width=device-width, minimal-ui shrink-to-fit=no" />
<meta http-equiv="cleartype" content="on" />
<!-- Add to homescreen for Chrome on Android -->
<meta name="mobile-web-app-capable" content="yes" />
<!-- For iOS web apps. Delete if not needed. -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Compulsivity" />
<link rel="stylesheet" href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" />
<!--
<script type="application/javascript" src="lib/touch-emulator.js"></script>
<script> TouchEmulator(); </script>
-->
<script type="application/javascript" src="https://cdn.jsdelivr.net/npm/jquery#2.2.4/dist/jquery.min.js"></script>
<script type="application/javascript" src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
</head>
<body>
<div id="header" data-role="header"><h4 style="color: #fff">Compulsivity</h4></div>
<div id="page-list" data-role="page">
<div data-role="content" role="main">
<ul data-role="listview" data-filter="true" data-inset="true"></ul>
</div>
</div>
<div id="footer" data-role="footer"><h4 style="color: #fff">Scroll</h4></div>
</body>
</html>
Among others, You need to be aware also of the pull-to-refresh and inertia (or momentum) of the smooth scroll behaviors.
Please, try to scroll or to swipe and look how the events are tracked: either the top bar or bottom bar will change color to display the user activity after reaching the bottom or the top respectively of the page.
JavaScript:
// get the button
var theBtn = document.getElementById('theBtn');
// get the box
var theBox = document.getElementById('theBox');
// add event to the button on click show/hide(toggle) the box
theBtn.addEventListener('click', () => {
theBox.classList.toggle('active');
});
// when scrolling on the box
theBox.onscroll = function(){
// get the top of the div
var theBoxTop = theBox.scrollTop;
if(theBoxTop <= 0){
// when it reaches 0 or less, hide the box. It'll toggle the class, since it's "show" will "hide"
theBox.classList.toggle('active');
}
};
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-size: 10px;
font-family: 'Arial', sans-serif;
height: 1500px;
}
html {
scroll-behavior: smooth;
}
ul {
list-style-type: none;
}
#theBox ul li {
border: 1px solid;
height: 100px;
}
#navbar-bottom {
height: 100px;
width: 100%;
background: rgb(90, 111, 143);
position: fixed;
bottom: 0;
left: 0;
right: 0;
box-shadow: 0 0 2px 2px rgba(90, 111, 143, 0.562);
display: flex;
justify-content: space-around;
align-items: center;
}
#theBox {
background-color: red;
height: 350px;
width: 100%;
position: fixed;
bottom: 0;
transform: translateY(100%);
transition: all 0.3s;
overflow-y: scroll;
}
#theBox.active{
transform: translateY(0);
}
.myBtns {
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
position: relative;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: pointer;
}
.myBtns span {
height: 3px;
width: 30px;
background-color: black;
margin: 3px 0;
}
<main role="main">
<div id="theBox">
<ul>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
</ul>
</div>
<div id="navbar-bottom">
<button class="myBtns"></button>
<button class="myBtns" id="theBtn">
<span></span>
<span></span>
<span></span>
</button>
<button class="myBtns"></button>
</div>
</main>
jQuery:
// add event to the button on click show/hide(toggle) the box
$('#theBtn').click(function(){
$('#theBox').toggleClass('active');
});
// when scrolling on the box
$('#theBox').scroll(function () {
// get the top of the div
var theBoxTop = $('#theBox').scrollTop();
// when it reaches 0 or less, hide the box. It'll toggle the class, since it's "show" will "hide"
if(theBoxTop <= 0){
$('#theBox').toggleClass('active');
}
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-size: 10px;
font-family: 'Arial', sans-serif;
height: 1500px;
}
html {
scroll-behavior: smooth;
}
ul {
list-style-type: none;
}
#theBox ul li {
border: 1px solid;
height: 100px;
}
#navbar-bottom {
height: 100px;
width: 100%;
background: rgb(90, 111, 143);
position: fixed;
bottom: 0;
left: 0;
right: 0;
box-shadow: 0 0 2px 2px rgba(90, 111, 143, 0.562);
display: flex;
justify-content: space-around;
align-items: center;
}
#theBox {
background-color: red;
height: 350px;
width: 100%;
position: fixed;
bottom: 0;
transform: translateY(100%);
transition: all 0.3s;
overflow-y: scroll;
}
#theBox.active{
transform: translateY(0);
}
.myBtns {
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
position: relative;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: pointer;
}
.myBtns span {
height: 3px;
width: 30px;
background-color: black;
margin: 3px 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<main role="main">
<div id="theBox">
<ul>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
<li><p>Text</p></li>
</ul>
</div>
<div id="navbar-bottom">
<button class="myBtns"></button>
<button class="myBtns" id="theBtn">
<span></span>
<span></span>
<span></span>
</button>
<button class="myBtns"></button>
</div>
</main>
window.onscroll = function(ev) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
alert("you are at the bottom of the page");
}
};
Link to demo: http://jsfiddle.net/5xpoe4yg/
There are two solutions for this. One is for touch devices and second for devices using mouse.
Using Wheel event
If target is a mouse device, then we will use following method:
document.onwheel = event => ScrollAction(event);
For more info on wheel event, please visit this link.
Touch Devices
If target is a touch device then following method will be useful:
document.ontouchcancel = event => TouchInterrupt(event);
document.ontouchend = event => FingerRemoved(event);
document.ontouchmove = event => FingerDragged(event);
document.ontouchstart = event => FingerPlaced(event);
For more info on touch events, please visit this link.
I think your problem fully is solved by this solution.
Your specific question is solveable by listening to the wheel event, although the result is not terribly precise. The wheel event often fires before the scroll event so this example will sometimes log negative scroll value on the first scroll up from the bottom of the page:
const content = document.querySelector('.content');
for (let i = 0; i < 50; i++) {
const p = document.createElement('p');
p.textContent = 'Content';
content.append(p);
};
content.addEventListener('wheel', e => {
const atBottom = content.scrollHeight - content.scrollTop === content.clientHeight;
if (atBottom) console.log(e.deltaY);
});
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
height: 100vh;
width: 100%;
}
.content {
overflow-y: scroll;
height: 100%;
}
<div class="content"></div>
As others have suggested, a better approach for your use case might instead be to have an overlay which you can trigger on click/touch and then scroll into view. One issue you might run into is that deeply nested scroll on web browsers can get real ugly real fast, without resorting to pure JS solutions which also have their own performance issues.
This is a popup that, when clicked on, opens and enables you to scroll. When it gets to the top of the page, it's header sticks.
var navbar = document.querySelector('.navbar'),
navheader = document.querySelector('.navheader');
// Toggle navbar
navheader.addEventListener('click', e => {
navbar.classList.toggle('open');
if (!navbar.classList.contains('open')) {
navbar.style.overflow = 'hidden';
document.body.style.overflow = '';
navbar.scrollTop = 0;
stickTop = false;
navbar.classList.remove('sticky');
navbar.style.top = '';
navbar.style.transition = '.2s';
setTimeout(() => {
navbar.style.transition = '';
}, 200);
}
else {
navbar.style.overflow = 'overlay';
navbar.style.transition = '.2s';
setTimeout(() => {
navbar.style.transition = '';
}, 200);
}
})
var prevtop = 0;
var stickTop = false;
// Add scroll listener
navbar.addEventListener('scroll', e => {
// If navbar is open
if (navbar.classList.contains('open')) {
if (!stickTop) {
navbar.style.top = navbar.getBoundingClientRect().top - navbar.scrollTop + 'px';
}
if ((window.innerHeight - navbar.getBoundingClientRect().bottom) >= 0) {
document.body.style.overflow = 'hidden';
navbar.style.overflow = 'auto';
navbar.style.top = 0;
navbar.classList.add('sticky');
stickTop = true;
}
if (navbar.scrollTop == 0) {
navbar.classList.remove('open');
navbar.style.overflow = 'hidden';
document.body.style.overflow = '';
stickTop = false;
navbar.classList.remove('sticky');
navbar.style.top = '';
navbar.style.transition = '.2s';
setTimeout(() => {
navbar.style.transition = '';
}, 200);
}
}
})
body {
font-family: sans-serif;
}
.navbar {
position: fixed;
top: calc(100vh - 50px);
height: 100vh;
left: 0;
width: 100%;
overflow: hidden;
}
.navbar.open {
top: 50vh;
}
.navcontent {
background: black;
width: 100%;
color: white;
}
.navcontent p {
margin: 0;
}
.navheader {
height: 50px;
width: 100%;
background: lightblue;
cursor: pointer;
top: 0;
position: sticky;
display: flex;
justify-content: center;
z-index: 1;
}
.navheader::before {
width: 50px;
height: 3px;
margin-top: 10px;
background: white;
border-radius: 3px;
content: '';
}
<div class="navbar">
<div class="navheader"></div>
<div class="navcontent"><p>S</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>E</p></div>
</div>
<div class="content">
<p>S</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>E</p>
</div>