Box-shadow responds to mouse position - javascript

I'm trying to create an animation in a webpage where an element's box-shadow responds to the mouse's position, i.e. (red X = mouse):
I already found a function that tracks the mouse movement, but I don't know how to apply this to the object. This is my code:
$(document).ready(function() {
function shadowAnimation() {
var objectToAnimate = $("#shadow-test");
document.onmousemove = handleMouseMove;
function handleMouseMove(event) {
var eventDoc, doc, body;
event = event || window.event;
if (event.pageX == null && event.clientX != null) {
eventDoc = (event.target && event.target.ownerDocument) || document;
doc = eventDoc.documentElement;
body = eventDoc.body;
event.pageX = event.clientX +
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY +
(doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0);
}
console.log(event.pageX + " " + event.pageY);
}
}
});
#shadow-test {
box-shadow: -10px -10px red;
border: 1px solid white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p id="shadow-test">This is a shadow test</p>

Your logic to capture the mouse position is much more complicated than it needs to be. You only need to track pageX and pageY of the mousemove event.
To move the shadow you simply need to work out the distance from the mouse to the mid-point of each dimension of the target element. Then you can apply that distance, limited by the available height of the element, to the size of the box-shadow, something like this:
jQuery($ => {
let $shadow = $('#shadow-test');
let shadowMax = $shadow.height();
let shadowMin = shadowMax * -1;
let shadowMidPoints = [
$shadow.offset().left + $shadow.width() / 2,
$shadow.offset().top + $shadow.height() / 2
];
$(document).on('mousemove', e => {
let shadowX = Math.min(shadowMax, Math.max(shadowMin, shadowMidPoints[0] - e.pageX));
let shadowY = Math.min(shadowMax, Math.max(shadowMin, shadowMidPoints[1] - e.pageY));
$shadow.css('box-shadow', `${shadowX}px ${shadowY}px red`);
});
});
body {
height: 2000px;
}
#shadow-test {
box-shadow: -10px -10px red;
border: 1px solid white;
margin: 100px;
background-color: #CCC;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p id="shadow-test">This is a shadow test</p>

Please take a look at the following examples(sans jquery).
const p = document.querySelector('#shadow-test');
const clamp = (a, m, n) => {
const max = Math.max(m, n);
const min = Math.min(m, n);
return Math.max(min, Math.min(max, a));
};
const MAX_SHADOW_OFFSET = 30;
const paint = (x, y) => {
const r = p.getBoundingClientRect();
const o = Math.min(r.width, r.height, MAX_SHADOW_OFFSET); // compute max shadow offset
const mx = clamp(x, r.left - o, r.right + o); // clamp mouse coordinates within the shadow projection bounding box.
const my = clamp(y, r.top - o, r.bottom + o);
const px = r.right - r.width / 2; // compute element bb midpoints.
const py = r.bottom - r.height / 2;
const nx = (mx - px) / (r.right - r.left + 2 * o); // project mouse position relative to the bounding box to [-.5, .5];
const ny = (my - py) / (r.bottom - r.top + 2 * o);
requestAnimationFrame(() => {
p.style.boxShadow = `${-1 * nx * o}px ${-1 * ny * o}px var(--shadow-color)`;
});
};
document.addEventListener('mousemove', (e) => paint(e.clientX, e.clientY), {
passive: true
});
:root {
--primary-color: black;
--secondary-color: white;
--shadow-color: red;
--button-color: blue;
--color: white;
}
body {
background-color: var(--primary-color);
padding: 1rem;
}
.center {
text-align: center;
}
button {
background-color: var(--button-color);
color: var(--color);
border: 0;
border-radius: .25rem;
padding: .375rem 1rem;
}
#shadow-test {
border: 1px solid var(--secondary-color);
color: var(--secondary-color);
}
<div class="center">
<button>
More
</button>
</div>
<p id="shadow-test">This is a shadow test</p>
Its definitely a mouthful compared to the other solutions but its not all that complex. The algorithm can be broken down into a couple of steps.
We figure out the bounding box of the element. We need this to figure out the max offset for our box shadow as well as projections to our normalize coordinate space later on.
We clamp the mouse coordinates to our new bounding box. This is used so that if you move your mouse far away from the element it doesn't continue moving. We also will need the mouse coordinates to project our to our normalized coordinate space.
We find the midpoint of our element. Anything left of the midpoint will be negative, anything right will be positive and so on. We can use the sign values to figure what side the box shadow should be. A mouse on the left side of the midpoint will give use a negative value, on the right side it will be positive.
Finally we project our mouse coordinates to our normalized coordinate space of [-0.5, .5]. The normalized coordinates makes it super easy to compute offsets by just simple multiplication. Think of (nx,ny) as just scale values. Given a max shadow offset value, how much should we apply to the style? and what direction?
Other notes
Im updating the style in an request animation frame. This is typically better for performance. You should also note the css variables. This is done so i don't have to hard code the color value of the box-shadow in code. The theme lives completely in css.

As I understand you implemented tracking of mouse.
In your screenshot and description its not clear when box-shadow property changes according to mouse position.
Once you catch mouse position and can calculate position of where mouse pointer is then just use this js code to update box-shadow property of your text input.
three case
$("#shadow-test").css('box-shadow', '-10px -10px red');
$("#shadow-test").css('box-shadow', '10px 10px red');
$("#shadow-test").css('box-shadow', '0px 10px red');

Related

How to rotate square following mouse by mouse movement angle?

I have square that follows my cursor.
Its border top is red to see if the rotation is right.
I'm trying to rotate it depending on mouse movement angle. Like if mouse goes 45deg top right then square must rotate by 45deg.
The problem is that when I move my mouse slowly the square starts to rotate like crazy. But if I move my mouse fast enough square rotates pretty smooth.
Actually it's just a part of my task that I'm trying to accomplish. My whole task is to make custom circle cursor that stretches when mouse moving. The idea I'm trying to implement:
rotate circle by mouse movement angle and then just scaleX it to make stretching effect. But I cannot do it because of problem I described above. I need my follower to rotate smoothly when mouse speed is slow.
class Cursor {
constructor() {
this.prevX = null;
this.prevY = null;
this.curX = null;
this.curY = null;
this.angle = null;
this.container = document.querySelector(".cursor");
this.follower = this.container.querySelector(".cursor-follower");
document.addEventListener("mousemove", (event) => {
this.curX = event.clientX;
this.curY = event.clientY;
});
this.position();
}
position(timestamp) {
this.follower.style.top = `${this.curY}px`;
this.follower.style.left = `${this.curX}px`;
this.angle = Math.atan2(this.curY - this.prevY, this.curX - this.prevX) * 180/Math.PI;
console.log(this.angle + 90);
this.follower.style.transform = `rotateZ(${this.angle + 90}deg)`;
this.prevX = this.curX;
this.prevY = this.curY;
requestAnimationFrame(this.position.bind(this));
}
}
const cursor = new Cursor();
.cursor-follower {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
pointer-events: none;
user-select: none;
width: 76px;
height: 76px;
margin: -38px;
border: 1.5px solid #000;
border-top: 1.5px solid red;
}
<div class="cursor">
<div class="cursor-follower"></div>
</div>
Following the cursor tangent smoothly isn't as simple as it first feels. In modern browsers mousemove event fires nearby at the frame rate (typically 60 FPS). When the mouse is moving slowly, the cursor moves only a pixel or two between the events. When calculating the angle, vertical + horizontal move of 1px resolves to 45deg. Then there's another problem, the event firing rate is not consistent, during the mouse is moving, event firing rate can drop to 30 FPS or even to 24 FPS, which actually helps to get more accurate angle, but makes the scale calculations heavily inaccurate (your real task seems to need scale calculations too).
One solution is to use CSS Transitions to make the animation smoother. However, adding a transition makes the angle calculations much more complex, because the jumps between negative and positive angles Math.atan2 returns when crossing PI will become visible when using transition.
Here's a sample code of how to use transition to make the cursor follower smoother.
class Follower {
// Default options
threshold = 4;
smoothness = 10;
stretchRate = 100;
stretchMax = 100;
stretchSlow = 100;
baseAngle = Math.PI / 2;
// Class initialization
initialized = false;
// Listens mousemove event
static moveCursor (e) {
if (Follower.active) {
Follower.prototype.crsrMove.call(Follower.active, e);
}
}
static active = null;
// Adds/removes mousemove listener
static init () {
if (this.initialized) {
document.removeEventListener('mousemove', this.moveCursor);
if (this.active) {
this.active.cursor.classList.add('hidden');
}
} else {
document.addEventListener('mousemove', this.moveCursor);
}
this.initialized = !this.initialized;
}
// Base values of instances
x = -1000;
y = -1000;
angle = 0;
restoreTimer = -1;
stamp = 0;
speed = [0];
// Prototype properties
constructor (selector) {
this.cursor = document.querySelector(selector);
this.restore = this.restore.bind(this);
}
// Activates a new cursor
activate (options = {}) {
// Remove the old cursor
if (Follower.active) {
Follower.active.cursor.classList.add('hidden');
Follower.active.cursor.classList.remove('cursor', 'transitioned');
}
// Set the new cursor
Object.assign(this, options);
this.setCss = this.cursor.style.setProperty.bind(this.cursor.style);
this.cursor.classList.remove('hidden');
this.cHW = this.cursor.offsetWidth / 2;
this.cHH = this.cursor.offsetHeight / 2;
this.setCss('--smoothness', this.smoothness / 100 + 's');
this.cursor.classList.add('cursor');
setTimeout(() => this.cursor.classList.add('transitioned'), 0); // Snap to the current angle
this.crsrMove({
clientX: this.x,
clientY: this.y
});
Follower.active = this;
return this;
}
// Moves the cursor with effects
crsrMove (e) {
clearTimeout(this.restoreTimer); // Cancel reset timer
const PI = Math.PI,
pi = PI / 2,
x = e.clientX,
y = e.clientY,
dX = x - this.x,
dY = y - this.y,
dist = Math.hypot(dX, dY);
let rad = this.angle + this.baseAngle,
dTime = e.timeStamp - this.stamp,
len = this.speed.length,
sSum = this.speed.reduce((a, s) => a += s),
speed = dTime
? ((1000 / dTime) * dist + sSum) / len
: this.speed[len - 1], // Old speed when dTime = 0
scale = Math.min(
this.stretchMax / 100,
Math.max(speed / (500 - this.stretchRate || 1),
this.stretchSlow / 100
)
);
// Update base values and rotation angle
if (isNaN(dTime)) {
scale = this.scale;
} // Prevents a snap of a new cursor
if (len > 5) {
this.speed.length = 1;
}
// Update angle only when mouse has moved enough from the previous update
if (dist > this.threshold) {
let angle = Math.atan2(dY, dX),
dAngle = angle - this.angle,
adAngle = Math.abs(dAngle),
cw = 0;
// Smoothen small angles
if (adAngle < PI / 90) {
angle += dAngle * 0.5;
}
// Crossing ±PI angles
if (adAngle >= 3 * pi) {
cw = -Math.sign(dAngle) * Math.sign(dX); // Rotation direction: -1 = CW, 1 = CCW
angle += cw * 2 * PI - dAngle; // Restores the current position with negated angle
// Update transform matrix without transition & rendering
this.cursor.classList.remove('transitioned');
this.setCss('--angle', `${angle + this.baseAngle}rad`);
this.cursor.offsetWidth; // Matrix isn't updated without layout recalculation
this.cursor.classList.add('transitioned');
adAngle = 0; // The angle was handled, prevent further adjusts
}
// Orthogonal mouse turns
if (adAngle >= pi && adAngle < 3 * pi) {
this.cursor.classList.remove('transitioned');
setTimeout(() => this.cursor.classList.add('transitioned'), 0);
}
rad = angle + this.baseAngle;
this.x = x;
this.y = y;
this.angle = angle;
}
this.scale = scale;
this.stamp = e.timeStamp;
this.speed.push(speed);
// Transform the cursor
this.setCss('--angle', `${rad}rad`);
this.setCss('--scale', `${scale}`);
this.setCss('--tleft', `${x - this.cHW}px`);
this.setCss('--ttop', `${y - this.cHH}px`);
// Reset the cursor when mouse stops
this.restoreTimer = setTimeout(this.restore, this.smoothness + 100, x, y);
}
// Returns the position parameters of the cursor
position () {
const {x, y, angle, scale, speed} = this;
return {x, y, angle, scale, speed};
}
// Restores the cursor
restore (x, y) {
this.state = 0;
this.setCss('--scale', 1);
this.scale = 1;
this.speed = [0];
this.x = x;
this.y = y;
}
}
Follower.init();
const crsr = new Follower('.crsr').activate();
body {
margin: 0px;
}
.crsr {
width: 76px;
height: 76px;
border: 2px solid #000;
border-radius: 0%;
text-align: center;
font-size: 20px;
}
.cursor {
position: fixed;
cursor: default;
user-select: none;
left: var(--tleft);
top: var(--ttop);
transform: rotate(var(--angle)) scaleY(var(--scale));
}
.transitioned {
transition: transform var(--smoothness) linear;
}
.hidden {
display: none;
}
<div class="crsr hidden">A</div>
The basic idea of the code is to wait until the mouse has moved enough pixels (threshold) to calculate the angle. The "mad circle" effect is tackled by setting the angle to the same position, but at the negated angle when crossing PI. This change is made invisibly between the renderings.
CSS variables are used for the actual values in transform, this allows to change a single parameter of the transform functions at the time, you don't have to rewrite the entire rule. setCss method is just syntactic sugar, it makes the code a little bit shorter.
The current parameters are showing a rectangle follower as it is in your question. Setting ex. stretchMax = 300 and stretchSlow = 125 and adding 50% border radius to CSS might be near to what you finally need. stretchRate defines the stretch related to the speed of the mouse. If the slow motion is still not smooth enough for your purposes, you can create a better algorithm to // Smoothen small angles section (in crsrMove method). You can play with the parameters at jsFiddle.
Try like this
class Cursor {
constructor() {
this.prevX = null;
this.prevY = null;
this.curX = null;
this.curY = null;
this.angle = null;
this.container = document.querySelector(".cursor");
this.follower = this.container.querySelector(".cursor-follower");
document.addEventListener("mousemove", (event) => {
this.curX = event.clientX;
this.curY = event.clientY;
});
this.position();
}
position(timestamp) {
this.follower.style.top = `${this.curY}px`;
this.follower.style.left = `${this.curX}px`;
if (this.curY !== this.prevY && this.curX !== this.prevX) {
this.angle = Math.atan2(this.curY - this.prevY, this.curX - this.prevX) * 180/Math.PI;
}
console.log(this.angle + 90);
this.follower.style.transform = `rotateZ(${this.angle + 90}deg)`;
this.prevX = this.curX;
this.prevY = this.curY;
requestAnimationFrame(this.position.bind(this));
}
}
const cursor = new Cursor();

Trying to get this smoother and more natural in behavior

My implementation,
http://kodhus.com/kodnest/land/PpNFTgp
I am curious, as I am not able for some reason to figure this out, how to get my JavaScript to make my slider behave more natural and smoother, if someone knows, how to, or can make this, feel free. I'd be happy to understand.
JavaScript:
const thumb = document.querySelector('.thumb');
const thumbIndicator = document.querySelector('.thumb .thumb-indicator');
const sliderContainer = document.querySelector('.slider-container');
const trackProgress = document.querySelector('.track-progress');
const sliderContainerStart = sliderContainer.offsetLeft;
const sliderContainerWidth = sliderContainer.offsetWidth;
var translate;
var dragging = false;
var percentage = 14;
document.addEventListener('mousedown', function(e) {
if (e.target.classList.contains('thumb-indicator')) {
dragging = true;
thumbIndicator.classList.add('focus');
}
});
document.addEventListener('mousemove', function(e) {
if (dragging) {
console.log('moving', e)
if (e.clientX < sliderContainerStart) {
translate = 0;
} else if (e.clientX > sliderContainerWidth + sliderContainerStart) {
translate = sliderContainerWidth;
} else {
translate = e.clientX - sliderContainer.offsetLeft;
}
thumb.style.transform = 'translate(-50%) translate(' + translate + 'px)';
trackProgress.style.transform = 'scaleX(' + translate / sliderContainerWidth + ')'
}
});
function setPercentage() {
thumb.style.transform = 'translate(-50%) translate(' + percentage/100 * sliderContainerWidth + 'px)';
trackProgress.style.transform = 'scaleX(' + percentage/100 + ')';
}
function init() {
setPercentage();
}
init();
document.addEventListener('mouseup', function(e) {
dragging = false;
thumbIndicator.classList.remove('focus');
});
EDIT: Is there a way to smoothly and naturally increment by one for every slow move?
Is it possible to make to behave as if, like when one clicks the progress bar so that it jumps there?
The kodhus site is very janky in my browser, so I can't tell if your code lacks responsiveness or whether it's the site itself. I feel that your code is a bit convoluted: translate and width / height are mixed unnecessarily; no need to use a dragging boolean when that information is always stored in the classlist. The following slider performs nicely, and has a few considerations I don't see in yours:
stopPropagation when clicking the .thumb element
drag stops if window loses focus
pointer-events: none; applied to every part of the slider but the .thumb element
let applySliderFeel = (slider, valueChangeCallback=()=>{}) => {
// Now `thumb`, `bar` and `slider` are the elements that concern us
let [ thumb, bar ] = [ '.thumb', '.bar' ].map(v => slider.querySelector(v));
let changed = amt => {
thumb.style.left = `${amt * 100}%`;
bar.style.width = `${amt * 100}%`;
valueChangeCallback(amt);
};
// Pressing down on `thumb` activates dragging
thumb.addEventListener('mousedown', evt => {
thumb.classList.add('active');
evt.preventDefault();
evt.stopPropagation();
});
// Releasing the mouse button (anywhere) deactivates dragging
document.addEventListener('mouseup', evt => thumb.classList.remove('active'));
// If the window loses focus dragging also stops - this can be a very
// nice quality of life improvement!
window.addEventListener('blur', evt => thumb.classList.remove('active'));
// Now we have to act when the mouse moves...
document.addEventListener('mousemove', evt => {
// If the drag isn't active do nothing!
if (!thumb.classList.contains('active')) return;
// Compute `xRelSlider`, which is the mouse position relative to the
// left side of the slider bar. Note that *client*X is compatible with
// getBounding*Client*Rect, and using these two values we can quickly
// get the relative x position.
let { width, left } = slider.getBoundingClientRect();
// Consider mouse x, subtract left offset of slider, and subtract half
// the width of the thumb (so drags position the center of the thumb,
// not its left side):
let xRelSlider = evt.clientX - left - (thumb.getBoundingClientRect().width >> 1);
// Clamp `xRelSlider` between 0 and the slider's width
if (xRelSlider < 0) xRelSlider = 0;
if (xRelSlider > width) xRelSlider = width;
// Apply styling (using percents is more robust!)
changed(xRelSlider / width);
evt.preventDefault();
evt.stopPropagation();
});
slider.addEventListener('mousedown', evt => {
let { width, left } = slider.getBoundingClientRect();
// Clicking the slider also activates a drag
thumb.classList.add('active');
// Consider mouse x, subtract left offset of slider, and subtract half
// the width of the thumb (so drags position the center of the thumb,
// not its left side):
let xRelSlider = evt.clientX - left - (thumb.getBoundingClientRect().width >> 1);
// Apply styling (using percents is more robust!)
changed(xRelSlider / width);
evt.preventDefault();
evt.stopPropagation();
});
changed(0);
};
let valElem = document.querySelector('.value');
applySliderFeel(document.querySelector('.slider'), amt => valElem.innerHTML = amt.toFixed(3));
.slider {
position: absolute;
width: 80%; height: 4px; background-color: rgba(0, 0, 0, 0.3);
left: 10%; top: 50%; margin-top: -2px;
}
.slider > .bar {
position: absolute;
left: 0; top: 0; width: 0; height: 100%;
background-color: #000;
pointer-events: none;
}
.slider > .thumb {
position: absolute;
width: 20px; height: 20px; background-color: #000; border-radius: 100%;
left: 0; top: 50%; margin-top: -10px;
}
.slider > .thumb.active {
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.5);
}
<div class="slider">
<div class="bar"></div>
<div class="thumb"></div>
</div>
<div class="value"></div>

How to test if a point is inside an SVG closed path

I'm trying to put together a drag & drop interface which allows a user to drag a div on a page which is constrained to the inside of an irregular closed SVG path.
Here's an example - the orange square is my draggable element, the gray SVG path is what I want to constrain it to on drop:
<div class="drag-parent">
<svg xmlns="http://www.w3.org/2000/svg" width="141.019" height="74.065" viewBox="0 0 141.019 74.065">
<defs>
<style>
.target {
fill: #333;
}
</style>
</defs>
<path id="Path_4569" data-name="Path 4569" class="target" d="M0,0H141.018V74.065h-24.27V37.033H10.88V12.971H0Z"/>
</svg>
<div class="draggable" style="width:20px;height:20px;background:orange;cursor:pointer;"></div>
</div>
What I'd like to do is check the draggable div as it's dragged, to make sure it's completely inside the closed path of my SVG.
I'm using GSAP Draggable to take care of actually dragging the element, but I'm stumped on how to test if it's inside that path or not.
So far I've tried isPointInFill however this seems to return true in chrome nomatter what I give it.
I've also tried using mouseenter / mouseleave events on the path which is a great starting point; but when you're dragging something those events don't fire since the mouse pointer is "ontop" of the dragged item rather than the SVG path.
What would be a good way to enforce bounds of an SVG path - or, is there a much simpler way to enforce irregular bounds on dragged items?
The main idea is to check if all 4 corners of the orange div are over the path. Maybe I've oversimplified the things since the drag_parent has `margin 0; padding:0;``
I hope this is what you were asking.
let D = false,// if D ids true you can drag
m = {},// the mouse position
thePath = document.querySelector("#Path_4569"),
draggable = document.querySelector("#draggable");
draggable.w = draggable.getBoundingClientRect().width;
draggable.h = draggable.getBoundingClientRect().height;
draggable.p0s = [[], [], [], []];//one array for every corner
draggable.delta = {};// distance between the click point and the left upper corner
draggable.addEventListener("mousedown", e => {
D = true;
draggable.delta = oMousePos(draggable, e);
});
drag_parent.addEventListener("mousemove", e => {
if (D == true) {
let counter = 0;// how many corners are in path
m = oMousePos(drag_parent, e);
draggablePoints(m);
draggable.style.left = draggable.p0s[0][0] + 1 + "px";
draggable.style.top = draggable.p0s[0][1] + 1 + "px";
draggable.p0s.map(p => {
if (document.elementFromPoint(p[0], p[1]) && document.elementFromPoint(p[0], p[1]).id == "Path_4569") {
counter++;
}
});
if (counter == 4) {// if all 4 corners are in path
thePath.setAttributeNS(null, "fill", "#777");
} else {
thePath.setAttributeNS(null, "fill", "black");
}
}
});
drag_parent.addEventListener("mouseup", e => {
D = false;
});
drag_parent.addEventListener("mouseleave", e => {
D = false;
});
function oMousePos(elmt, evt) {
var ClientRect = elmt.getBoundingClientRect();
return {
//objeto
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
};
}
function draggablePoints(m) {
//top left
draggable.p0s[0][0] = m.x - draggable.delta.x - 1;
draggable.p0s[0][1] = m.y - draggable.delta.y - 1;
//top right
draggable.p0s[1][0] = m.x - draggable.delta.x + draggable.w + 1;
draggable.p0s[1][1] = m.y - draggable.delta.y + 1;
//bottom right
draggable.p0s[2][0] = m.x - draggable.delta.x + draggable.w + 1;
draggable.p0s[2][1] = m.y - draggable.delta.y + draggable.h + 1;
//bottom left
draggable.p0s[3][0] = m.x - draggable.delta.x + 1;
draggable.p0s[3][1] = m.y - draggable.delta.y + draggable.h + 1;
}
*{margin:0;padding:0}
svg {
outline: 1px solid;
}
#drag_parent {
outline: 1px solid;
min-height: 100vh;
position:relative;
}
#draggable {
position: absolute;
width: 20px;
height: 20px;
background: orange;
cursor: pointer;
}
<div id="drag_parent">
<svg xmlns="http://www.w3.org/2000/svg" width="141.019" height="74.065" viewBox="0 0 141.019 74.065">
<path id="Path_4569" data-name="Path 4569" class="target" d="M0,0H141.018V74.065h-24.27V37.033H10.88V12.971H0Z"/>
</svg>
<div id="draggable"></div>
</div>

Adding image on Fabric.js canvas at mouse coords results in offset image position

I have a rather trivial example where I'm attempting to insert an image on a Fabric.js canvas with the image centered at the mouse coordinates. The center of the cursor should be the exact center of the image.
I calculate the center of the image by halving its width and height and apply them as offsets to the left and top coordinates of the canvas.Image I'm inserting. Simple enough:
// Coordinates from the mouse click event.
// `x` and `y` are aliases for `clientX`, `clientY`, respectively
const x = event.e.x,
y = event.e.y
image.set({
left: x - (image.width / 2),
top: y - (image.height / 2)
})
canvas.add(image);
When the image is added, it's not quite center. In fact, there's a difference of 8 pixels on both x and y axes that I cannot account for.
This is the result:
The center of the image should be under the cursor.
If I manually set the offset to 28, the image is properly centered. But since I cannot account for the 8 extra pixels, this hack is unacceptable.
Working sample:
const canvas = new fabric.Canvas('c', { width: 400, height: 150 });
// Add an image centered x & y at the exact point the user clicks.
canvas.on('mouse:up', (opt) => {
canvas.clear();
const x = opt.e.x,
y = opt.e.y;
fabric.Image.fromURL('https://i.imgur.com/htyNxF6.png', (image) => {
/* Calculate the offset based on the image dimensions:
This does not work as expected. A 40x40 image has a x/y offsets of 20.
If we set the offset to 28, the image is centered at the cursor. Why 28?
Check the "Apply ..." checkbox to see this in action.
*/
const offsetX = chkOffset.checked ? 28 : (image.width / 2),
offsetY = chkOffset.checked ? 28 : (image.height / 2);
const left = x - offsetX,
top = y - offsetY;
image.set({
left: left,
top: top,
stroke: 0,
padding: 0,
centeredScaling: true,
hasControls: false,
strokeWidth: 0,
hasBorders: 0
});
canvas.add(image);
writeDebug(`Mouse at: x=${x}, y=${y};
Image placed at: x=${left}, y=${top}
Difference of ${Math.abs(left-x)}, ${Math.abs(top-y)}`);
});
// Show coordinates on mouse move
canvas.on('mouse:move', (opt) => {
const x = opt.e.x,
y = opt.e.y;
writeDebug(`Mouse coordinates: x=${x}, y=${y}`);
});
});
function writeDebug(message) {
document.getElementById('debug').innerText = message;
}
body {
font-family: Consolas;
}
#c {
border: 1px solid #ececec;
box-shadow: 2px 2px 5px #c0c0c0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.1.0/fabric.min.js"></script>
<canvas id="c"></canvas>
<label for="chkOffset">
<input type="checkbox" id="chkOffset" />
Apply 28px offset
</label>
<div id="debug">Click in the canvas to add the image</div>
When I was adding sample code to my question, I stumbled upon the answer. I set the padding of every element to 10 using CSS (* { padding: 10px }) to make everything look better and upon doing so discovered that the offset gap had increased.
Getting mouse coordinates using MouseEvent.e.x and MouseEvent.x.y are the culprit. I compared their values with MouseEvent.e.offsetX and MouseEvent.e.offsetY and found that the x and y values did not account for padding and margins.
Using offsetX and offsetY seemed to do the trick.
It seems if you want to get relevant mouse coordinates in Fabric.js, using offsetX and offsetY is the best bet as it accounts for offsets caused by CSS.
const canvas = new fabric.Canvas('c', { width: 400, height: 150 });
// Add an image centered x & y at the exact point the user clicks.
canvas.on('mouse:up', (opt) => {
canvas.clear();
// Use .offset instead:
const x = opt.e.offsetX,
y = opt.e.offsetY;
fabric.Image.fromURL('https://i.imgur.com/htyNxF6.png', (image) => {
const offsetX = (image.width / 2),
offsetY = (image.height / 2);
const left = x - offsetX,
top = y - offsetY;
image.set({
left: left,
top: top,
stroke: 0,
padding: 0,
centeredScaling: true,
hasControls: false,
strokeWidth: 0,
hasBorders: 0
});
canvas.add(image);
writeDebug(`Mouse at: x=${x}, y=${y};
Image placed at: x=${left}, y=${top}
Difference of ${Math.abs(left-x)}, ${Math.abs(top-y)}`);
});
// Show coordinates on mouse move
canvas.on('mouse:move', (opt) => {
const x = opt.e.offsetX,
y = opt.e.offsetY;
writeDebug(`Mouse coordinates: x=${x}, y=${y}`);
});
});
function writeDebug(message) {
document.getElementById('debug').innerText = message;
}
body {
font-family: Consolas;
}
#c {
border: 1px solid #ececec;
box-shadow: 2px 2px 5px #c0c0c0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.1.0/fabric.min.js"></script>
<canvas id="c"></canvas>
<div id="debug">Click in the canvas to add the image</div>

How to recognize hover/click of a border in CSS and JS

Hi I am working on a CodePen to make a draggable and resizavle box without Jquery. What I am trying to accomplish is that when you hover/click over the main body of the div you can drag it around and will have the "move" cursor.
When you hover/click on the border, you can resize the box, based on which side you click on and have the "col-resize" or "row-resize" cursor.
What I am really wondering is if it is even possible to select the border with JS and CSS, and if so how.
This is an example of how to determine which border you're hovering (good luck with your other calculations):
var div = document.querySelector("#div");
var delta = 10; // the thickness of the hovered border area
div.onmousemove = function(e) {
var rect = div.getBoundingClientRect();
var x = e.clientX - rect.left, // the relative mouse postion to the element
y = e.clientY - rect.top, // ...
w = rect.right - rect.left, // width of the element
h = rect.bottom - rect.top; // height of the element
var c = ""; // which cursor to use
if(y < delta) c += "n"; // north
else if( y > h - delta) c += "s"; // south
if(x < delta) c += "w"; // west
else if(x > w - delta) c += "e"; // east
if(c) // if we are hovering at the border area (c is not empty)
div.style.cursor = c + "-resize"; // set the according cursor
else // otherwise
div.style.cursor = "pointer"; // set to pointer
}
#div {
background-color: red;
width: 100px;
height: 100px;
}
<div id="div"></div>
<br>Psst! Hover at the border area! Corners too.
Note: The above method doesn't relly on wether or not the element has borders, and wether or not it could have child nodes (for example img...).
I don't think that you can detect a click just on the border without any workaround. You could detect the border by checking where the mouse is in relation to box, as #ibrahim mahrir did, but I prefer just using a wrapper element it:
Undynamic to CSS values
The most simple. Set the width of the "border" manually. Use if you're never going to change the padding/width of the "border".
var border = document.getElementById("border");
var bor = 4;
border.onclick = function(e) {
if (e.target !== e.currentTarget) return;
console.log("border-clicked")
}
border.onmouseover = function(e) {
y = e.offsetY;
if (y <= bor || y >= this.offsetHeight - bor) c = "row"
else c = "col"
this.style.cursor = c + "-resize";
}
#border {
padding: 4px;
background: blue;
box-sizing: border-box;
}
.box{
height: 100px;
width: 100%;
background: white;
cursor: default;
}
<div id="border">
<div class="box"></div>
</div>
Dynamic to one CSS value
Set the width of the "border" by selecting one of the values of the padding. Use this if are going to change the value of the padding and has the same width throughout.
var border = document.getElementById("border");
var bor = parseInt(window.getComputedStyle(border, null).getPropertyValue('padding-left') )
border.onclick = function(e) {
if (e.target !== e.currentTarget) return;
console.log("border-clicked")
}
border.onmouseover = function(e) {
y = e.offsetY;
if (y <= bor || y >= this.offsetHeight - bor) c = "row"
else c = "col"
this.style.cursor = c + "-resize";
}
#border {
padding: 4px;
background: blue;
box-sizing: border-box;
}
.box{
height: 100px;
width: 100%;
background: white;
cursor: default;
}
<div id="border">
<div class="box"></div>
</div>
Dynamic to both CSS values
Set the width of the "border" by selecting a horizontal and vertical value of the padding. Use this if the padding is like padding: #px #px.
var border = document.getElementById("border");
var borderStyles = window.getComputedStyle(border, null);
var borLeft = parseInt( borderStyles.getPropertyValue('padding-left') )
var borTop = parseInt( borderStyles.getPropertyValue('padding-top') )
border.onclick = function(e) {
if (e.target !== e.currentTarget) return;
console.log("border-clicked")
}
border.onmouseover = function(e) {
x = e.offsetX;
y = e.offsetY;
if (x < borLeft || x > this.offsetWidth - borLeft ) c = "col";
else if (y <= borTop || y >= this.offsetHeight - borTop) c = "row"
this.style.cursor = c + "-resize";
}
#border {
padding: 4px;
background: blue;
box-sizing: border-box;
}
.box{
height: 100px;
width: 100%;
background: white;
cursor: default;
}
<div id="border">
<div class="box"></div>
</div>
Create a div and make it is position absolute inside your hole div add the style of you border to it then give a hover effect and the event you want to it.
Set the position relative to the hole div and absolute to the border div you have to set left, right,top,bottom to minus value according to your border width you may need to make the background-color as transparent.

Categories

Resources