Reputation: 11
I trying to create a mouse trail using svg path elements.
used beziers for smoothening but somehow smoothening is not good.
can some one help me with this.
const smoothing = 0.10;
function line(pointA, pointB) {
// Calculate the horizontal distance between pointA and pointB
var lengthX = pointB[0] - pointA[0];
// Calculate the vertical distance between pointA and pointB
var lengthY = pointB[1] - pointA[1];
// Calculate the length of the line segment using Pythagoras theorem
var length = Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2));
// Calculate the angle of the line segment relative to the x-axis
var angle = Math.atan2(lengthY, lengthX);
// Return an object containing the length and angle of the line segment
return {
length: length,
angle: angle
};
}
function controlPoint(current, previous, next, reverse) {
// Use previous point as fallback if it doesn't exist
var p = previous || current;
// Use next point as fallback if it doesn't exist
var n = next || current;
// Calculate the line between previous and next points
var o = line(p, n);
// Calculate angle based on line's angle and whether to reverse
var angle = o.angle + (reverse ? Math.PI : 0);
// Calculate length with smoothing factor
var length = o.length * smoothing;
// Calculate new control point coordinates
var x = current[0] + Math.cos(angle) * length;
var y = current[1] + Math.sin(angle) * length;
return [x, y]; // Return the control point coordinates
}
function bezierCommand(point, i, points) {
// Get the control points
var cps = controlPoint(points[i - 1], points[i - 2], point);
var cpe = controlPoint(point, points[i - 1], points[i + 1], true);
// Construct and return the Bezier command string
var command = "C " + cps[0] + "," + cps[1] + " " + cpe[0] + "," + cpe[1] + " " + point[0] + "," + point[1];
return command;
}
function svgPath(points, command) {
// Initialize the 'd' attribute string
var d = "";
// Loop over the points
for (var i = 0; i < points.length; i++) {
var point = points[i];
// If it's the first point, start with 'M'
if (i === 0) {
d += "M " + point[0] + "," + point[1];
} else {
// Otherwise, append the command for the current point
d += " " + command(point, i, points);
}
}
// Return the SVG path string
return '<path d="' + d + '" fill="none" stroke="red" class="paths"/>';
}
var lastMousePoints = [];
const svg = document.querySelector('#mouse-trail')
function addPoint(x, y) {
lastMousePoints.push([x, y]);
if (lastMousePoints.length > 20) {
lastMousePoints.shift();
svg.innerHTML = svgPath(lastMousePoints, bezierCommand)
} else if (lastMousePoints.length === 2) {
svg.innerHTML = svgPath(lastMousePoints, bezierCommand)
} else if (lastMousePoints.length > 2) {
svg.innerHTML = svgPath(lastMousePoints, bezierCommand)
}
}
document.addEventListener('mousemove', function(e) {
addPoint(e.pageX, e.pageY);
});
.paths {
stroke-width: 1.5;
}
<svg id="mouse-trail" width="100%" height="100vh"></svg>
I'm storing the last 20 points of mouse and creating the path with the coordinates.
any other approaches are welcome too.
Note : should not use external libraries for this, I'm trying to create laser pointer effect like the one in excalidraw and google slides.
Upvotes: 1
Views: 97
Reputation: 17316
You can adapt @ConnorsFan's polyline smoothing concept from this answer SVG smooth freehand drawing.
Basically you're collecting multiple mouse input coordinates in a buffer. The drawn polyline is based on averaged coordinates – this will smooth jitters in the original point array.
Bigger buffer sizes will increase smoothing but also add an input lag.
I highly recommend to convert screen to SVG coordinates. Otherwise you may encounter problems when the SVG's viewBox size don't match the rendered layout size.
let svg = document.querySelector("#mouse-trail");
// collect coordinates
let lastMousePoints = [];
let maxVertices = 32;
// create buffer for polyline smoothing
let buffer = [];
let bufferSize = 8;
svg.addEventListener("mousemove", function(e) {
addPoint(e);
});
function addPoint(e) {
// get point in svg coordinate system
let pt = getMouseOrTouchPos(svg, e);
// append to buffer for smoothing
buffer.push(pt);
// start with lower buffer length if buffer not filled
if (buffer.length < bufferSize && buffer.length > 4) {
// get smoothed coordinates
pt = getAveragePoint(buffer, 4);
lastMousePoints.push(pt);
}
if (buffer.length > bufferSize) {
// get smoothed coordinates
pt = getAveragePoint(buffer, bufferSize);
lastMousePoints.push(pt);
// reset buffer
buffer.shift();
}
// remove points
if (lastMousePoints.length > maxVertices) {
lastMousePoints.shift();
}
// update polyline
polyline.setAttribute(
"points",
lastMousePoints
.map((pt) => {
return [pt.x, pt.y].join(' ');
})
.join(" ")
);
}
/**
* based on @ConnorsFan's answer
* SVG smooth freehand drawing
* https://stackoverflow.com/questions/40324313/svg-smooth-freehand-drawing/#40700068
*/
// Calculate the average point, starting at offset in the buffer
function getAveragePoint(buffer, bufferSize) {
var len = buffer.length;
let offset = Math.floor(len / 4);
offset = len > 8 ? len - 2 : 0
offset = 0;
if (len % 2 === 1 || len >= bufferSize) {
var totalX = 0;
var totalY = 0;
var pt, i;
var count = 0;
for (i = offset; i < len; i++) {
count++;
pt = buffer[i];
totalX += pt.x;
totalY += pt.y;
}
return {
x: totalX / count,
y: totalY / count
};
}
return null;
}
/**
* based on:
* @Daniel Lavedonio de Lima
* https://stackoverflow.com/a/61732450/3355076
*/
function getMouseOrTouchPos(svg, e) {
let x, y;
// touch cooordinates
if (
e.type == "touchstart" ||
e.type == "touchmove" ||
e.type == "touchend" ||
e.type == "touchcancel"
) {
let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0];
x = touch.pageX;
y = touch.pageY;
} else if (
e.type == "mousedown" ||
e.type == "mouseup" ||
e.type == "mousemove" ||
e.type == "mouseover" ||
e.type == "mouseout" ||
e.type == "mouseenter" ||
e.type == "mouseleave"
) {
x = e.clientX;
y = e.clientY;
}
// get svg user space coordinates
let point = new DOMPoint(x, y);
let ctm = svg.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
return {
x: point.x,
y: point.y
};
}
svg {
border: 1px solid #ccc;
}
polyline {
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
stroke: red;
fill: none;
}
<svg id="mouse-trail" width="100%" height="100vh">
<polyline id="polyline" pathLength="100" />
</svg>
Upvotes: 0