Zoom Into the Area Cursor Points - javascript

Refering to codepen demo here, I created this zoom in and zoom out function which works fine.
May I ask the code to zoom in and out of the area which my cursor points using the mouse scroll?
For example, if my cursor is moved to the green area in my picture and when I scoll the mouse, it will zoom into and out of the specific area? Any help will be very much appreicated :)
<div onclick="zoomin()" style="display: block;float: left;border: 1px solid;cursor: pointer">
ZoomIn
</div>
<div onclick="zoomout()" style="display: block;float: left;border: 1px solid;cursor: pointer;margin-left: 10px">
ZoomOut
</div>
var svg=document.getElementById('mainsvg');
var gnode=document.getElementById('gnode');
var zoomPercentage=0.25;
var MAXIMUM_ZOOM_HEIGHT = 1400;
var baseBox={};
var level=0;
var widthRatio,heightRatio;
var clientheight = document.documentElement.clientHeight;
var clientwidth = document.documentElement.clientWidth;
function setup(){
var
baseX,
baseY,
baseWidth,
baseHeight,
percentageDifference,
heightDifference;
svg.setAttribute('height', clientheight);
svg.setAttribute('width', clientwidth);
var boundry=document.getElementById('boundry');
boundry.setAttribute('height', clientheight-1);
boundry.setAttribute('width', clientwidth-1);
var centernode=document.getElementById('centernode');
centernode.setAttribute('cy', clientheight/2);
centernode.setAttribute('cx', clientwidth/2);
if (svg.height.baseVal.value >= MAXIMUM_ZOOM_HEIGHT)
baseHeight = MAXIMUM_ZOOM_HEIGHT;
else
baseHeight = Math.round(gnode.getBBox().height) + 60;
baseY = (svg.height.baseVal.value - baseHeight) / 2;
percentageDifference = baseHeight / svg.height.baseVal.value;
baseWidth = percentageDifference * svg.width.baseVal.value;
baseX = (svg.width.baseVal.value - baseWidth) / 2;
baseBox.x = baseX;
baseBox.y = baseY;
baseBox.width = baseWidth;
baseBox.height = baseHeight;
level = 0;
heightDifference = MAXIMUM_ZOOM_HEIGHT - baseHeight;
zoomPercentage = (heightDifference / 10) / heightDifference;
setViewBox(baseBox);
}
function setViewBox(viewBox) {
svg.viewBox.baseVal.x = Math.round(viewBox.x);
svg.viewBox.baseVal.y = Math.round(viewBox.y);
svg.viewBox.baseVal.width = Math.round(viewBox.width);
svg.viewBox.baseVal.height = Math.round(viewBox.height);
setRatios();
}
function setRatios () {
widthRatio = svg.viewBox.baseVal.width / svg.width.baseVal.value;
heightRatio = svg.viewBox.baseVal.height / svg.height.baseVal.value;
}
function calculateViewBox(level) {
var
height = baseBox.height - (zoomPercentage * level * baseBox.height),
y = baseBox.y + (baseBox.height - height) / 2,
width = baseBox.width - (zoomPercentage * level * baseBox.width),
x = baseBox.x + (baseBox.width - width) / 2,
viewBox = {
x: x,
y: y,
width: width,
height: height
}
return viewBox;
}
function zoomin(){
level++;
if(level>5)
level=5;
var
x,
y,
paperViewBox = svg.viewBox.baseVal,
previousViewBox = calculateViewBox(level - 1),
newViewBox = calculateViewBox(level);
//callback = this.afterZoom;
if (Math.round(paperViewBox.x) > Math.round(newViewBox.x))
/**
* is panned left
*/
x = paperViewBox.x - (previousViewBox.width - newViewBox.width) / 2;
else if (Math.round(paperViewBox.x) < Math.round(previousViewBox.x) - (Math.round(newViewBox.x) - Math.round(previousViewBox.x)))
/**
* is panned right
*/
x = paperViewBox.x + (previousViewBox.width - newViewBox.width) + (previousViewBox.width - newViewBox.width) / 2;
else
x = newViewBox.x;
if (Math.round(paperViewBox.y) > Math.round(newViewBox.y))
/**
* is panned up
*/
y = paperViewBox.y - (previousViewBox.height - newViewBox.height) / 2;
else if (Math.round(paperViewBox.y) < Math.round(previousViewBox.y) - (Math.round(newViewBox.y) - Math.round(previousViewBox.y)))
/**
* is panned down
*/
y = paperViewBox.y + (previousViewBox.height - newViewBox.height) + (previousViewBox.height - newViewBox.height) / 2;
else
y = newViewBox.y;
var data = {
viewBox: {
x: x,
y: y,
width: newViewBox.width,
height: newViewBox.height
}
}
SetZoomViewBox(data);
}
function SetZoomViewBox(data){
var viewBox = data.viewBox;
svg.viewBox.baseVal.x = Math.round(viewBox.x);
svg.viewBox.baseVal.y = Math.round(viewBox.y);
svg.viewBox.baseVal.width = Math.round(viewBox.width);
svg.viewBox.baseVal.height = Math.round(viewBox.height);
setRatios();
}
function zoomout(){
level--;
if(level<0)
level=0;
var
x,
y,
paperViewBox = svg.viewBox.baseVal,
previousViewBox = calculateViewBox(level + 1),
newViewBox = calculateViewBox(level);
if (Math.round(paperViewBox.x) > Math.round(previousViewBox.x) + (Math.round(previousViewBox.x) - Math.round(newViewBox.x)))
/**
* is panned left
*/
x = paperViewBox.x - (newViewBox.width - previousViewBox.width);
else if (Math.round(paperViewBox.x) < Math.round(previousViewBox.x))
/**
* is panned right
*/
x = paperViewBox.x;
else
x = newViewBox.x;
if (Math.round(paperViewBox.y) > Math.round(previousViewBox.y) + (Math.round(previousViewBox.y) - Math.round(newViewBox.y)))
/**
* is panned up
*/
y = paperViewBox.y - (newViewBox.height - previousViewBox.height);
else if (Math.round(paperViewBox.y) < Math.round(previousViewBox.y))
/**
* is panned down
*/
y = paperViewBox.y;
else
y = newViewBox.y;
var data = {
viewBox: {
x: x,
y: y,
width: newViewBox.width,
height: newViewBox.height
}
}
SetZoomViewBox(data);
}
setup();
<div onclick="zoomin()" style="display: block;float: left;border: 1px solid;cursor: pointer">
ZoomIn
</div>
<div onclick="zoomout()" style="display: block;float: left;border: 1px solid;cursor: pointer;margin-left: 10px">
ZoomOut
</div>
<svg id="mainsvg" width="600px" height="500px" viewBox="0 0 600 500">
<g id="gnode">
<rect id="boundry" x="0" y="0" width="599" height="499" fill="none" stroke='black'/>
<circle id="centernode" cx="300" cy="250" r="5" fill="red" stroke="none" />
<rect id="selected" x="450" y="100" width="50" height="50" fill="blue" stroke='none'/>
</g>
</svg>

You can add the following code to handle zoom in and zoom out based on mouse wheel event:
svg.addEventListener("wheel", function(event) {
if (event.deltaY < 0) {
zoomin();
} else if (event.deltaY > 0) {
zoomout();
}
});
Place this code after the setup function. It will handle the zoom in and zoom out events when the mouse wheel is scrolled over the svg element.

Related

why is my clientwidth/100 different than my width by percent?

I have a small program that when you press down on ArrowDown, the circle should move downwards from its position on screen. The issue I have is that my initial positioning for my svg circle object set by cx: 5% and cy:5% are different than my 5 * xshift and 5 * yshift which are derived from document.getElementById("test").clientWidth/100 and document.getElementById("test").clientHeight/100 which I would believe to be a good conversion to percentage. However when the program is run, the initial ArrowDown corrects the initial placement and creates a noticable shifted difference.
var goal = {x: window.innerWidth, y: window.innerHeight}
var xshift = document.getElementById("test").clientWidth/100
var yshift = document.getElementById("test").clientHeight/100
var player= {type:"Player", x:5 * xshift, y: 5 * yshift}
document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowDown'){
player.y += yshift;
if(player.y > (100 * yshift) -(9 * yshift)){
player.y = 91 * yshift;
}
//document.getElementById("UI").innerHTML = "Player position is x: " +player.x + " y: " + player.y;
document.getElementById('circe').setAttribute("transform", 'translate('+player.x+","+player.y+")");
}
}, true);
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Basic Snake Game</title>
<style>
html, body {
margin: 0;
padding: 0;
border: 0;
height: 100%
}
svg {
position: absolute;
margin: 0;
padding: 0;
border: 0;
background-color: blueviolet;
}
</style>
</head>
<body>
<!-- <h1 id="UI">Player position is x: 0 y: 0</h1> -->
<svg id="test" width="100%" height="100%">
<circle id="circe" cx="5%" cy="5%" r="5%" stroke="green" stroke-width="0%" fill="yellow" transform="translate(0,0)" />
</svg>
<script src="test.js"></script>
</body>
</html>
You can check complete code from: this fiddle, I combined your pieces of code and added a single line.
`
var goal = {x: window.innerWidth, y: window.innerHeight}
var xshift = document.getElementById("test").clientWidth/100
var yshift = document.getElementById("test").clientHeight/100
var player= {type:"Player", x:5 * xshift, y: 5 * yshift}
document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowDown'){
doShift();
}
}, true);
function doShift(){
player.y += yshift;
if(player.y > (100 * yshift) -(9 * yshift)){
player.y = 91 * yshift;
}
//document.getElementById("UI").innerHTML = "Player position is x: " +player.x + " y: " + player.y;
document.getElementById('circe').setAttribute("transform", 'translate('+player.x+","+player.y+")");
}
//document.addEventListener("onready", ()=> doShift());
//document.ondomcontentready=doShift;
doShift();
`;
Although it looks like a hack, the final doShift(); call creates the same effect as when you first press the down button - you specify what you want.
They are different because you are using a transform. The transform is being applied on top of (in addition to) the cx and cy coordinates.
In other words, the circle is being positioned at:
x = cx + translate.x = 5% + 5%
y = cy + (n * translate.y) = 5% + (n * 5%)
One way to fix this is to forget the transform attribute and just update the cx and cy attributes instead. Eg.
document.getElementById('circe').setAttribute("cy", player.y);
Demo
var goal = {x: window.innerWidth, y: window.innerHeight}
var xshift = document.getElementById("test").clientWidth/100
var yshift = document.getElementById("test").clientHeight/100
var player= {type:"Player", x:5 * xshift, y: 5 * yshift}
document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowDown'){
player.y += yshift;
if(player.y > (100 * yshift) -(9 * yshift)){
player.y = 91 * yshift;
}
//document.getElementById("UI").innerHTML = "Player position is x: " +player.x + " y: " + player.y;
document.getElementById('circe').setAttribute("cy", player.y);
}
}, true);
html, body {
margin: 0;
padding: 0;
border: 0;
height: 100%;
}
svg {
position: absolute;
margin: 0;
padding: 0;
border: 0;
background-color: blueviolet;
}
<!-- <h1 id="UI">Player position is x: 0 y: 0</h1> -->
<svg id="test" width="100%" height="100%">
<circle id="circe" cx="5%" cy="5%" r="5%" stroke="green" stroke-width="0%" fill="yellow" transform="translate(0,0)" />
</svg>

jQuery & Snap.svg -> circle not following mouse correctly

I am playing around with Snap.svg and jQuery a little bit, and I am creating this bitmoji and trying to make his eyes follow the mouse cursor.
It's all going pretty well except for the eyes. They are transforming and rotating when moving the cursor but not 100% correctly and I can't figure out why.
Here is my code in a JSFiddle: http://jsfiddle.net/bmp5j4x9/1/
Resize the result box, make it bigger and move around your mouse, I guess you'll see what I mean. Or take a look at http://dante-c.be.
This is the jQuery part:
var s = Snap(420, 420).attr({ viewBox: "0 0 120 120" });
$(s.node).appendTo(".button");
var image = s.paper.image('https://render.bitstrips.com/v2/cpanel/10220069-circle-357822728_5-s4-v1.png?palette=1', 0, 0, 1, 1);
image = image.pattern().attr({
patternContentUnits: "objectBoundingBox",
patternUnits: "",
width: "100%", height: "100%", viewBox: ""
});
var bitmojiCircle = s.circle(60, 60, 39).attr({ fill: image });
var circleX = 50, circleY = 63, circleRadius = 4.5;
var bigEyeCircle = s.circle(circleX, circleY, circleRadius);
var L1 = s.path("M "+circleX+" "+circleY +"L 0 0").attr({stroke: "blue"});
bigEyeCircle.attr({
fill: "#bada55",
stroke: "#000",
strokeWidth: 1
});
var smallEyeCircle = s.circle(0,0,3.5).attr({ fill: "red" });
var opacityCircle = s.circle(60, 60, 39).attr({ fill: "rgba(255,255,255,0.7)" });
var menuButton = s.path("M58.486 56.324H57.19c-.48 0-.866.387-.866.865v1.29c0 .48.387.86.865.86h1.29c.48 0 .86-.39.86-.87v-1.29c0-.48-.39-.87-.87-.87zm-4.324 0h-1.297c-.478 0-.865.387-.865.865v1.29c0 .48.387.86.865.86h1.297c.478 0 .865-.39.865-.87v-1.29c0-.48-.387-.87-.865-.87zM58.486 52H57.19c-.48 0-.866.387-.866.865v1.297c0 .478.387.865.865.865h1.29c.48 0 .86-.387.86-.865v-1.297c0-.478-.39-.865-.87-.865zm-4.324 0h-1.297c-.478 0-.865.387-.865.865v1.297c0 .478.387.865.865.865h1.297c.478 0 .865-.387.865-.865v-1.297c0-.478-.387-.865-.865-.865zm12.973 4.324h-1.297c-.478 0-.865.387-.865.865v1.29c0 .48.387.86.865.86h1.297c.478 0 .865-.39.865-.87v-1.29c0-.48-.387-.87-.865-.87zm-4.324 0h-1.29c-.48 0-.86.387-.86.865v1.29c0 .48.39.86.87.86h1.3c.48 0 .87-.39.87-.87v-1.29c0-.48-.38-.87-.86-.87zM67.14 52h-1.3c-.48 0-.866.387-.866.865v1.297c0 .478.387.865.865.865h1.29c.48 0 .86-.387.86-.865v-1.297c0-.478-.39-.865-.87-.865zm-4.324 0H61.52c-.48 0-.865.387-.865.865v1.297c0 .478.386.865.865.865h1.297c.48 0 .866-.387.866-.865v-1.297c0-.478-.386-.865-.864-.865zM58.49 64.973h-1.3c-.48 0-.866.387-.866.865v1.297c0 .478.387.865.865.865h1.29c.48 0 .86-.387.86-.865v-1.297c0-.478-.39-.865-.87-.865zm-4.325 0h-1.297c-.478 0-.865.387-.865.865v1.297c0 .478.387.865.865.865h1.297c.478 0 .865-.387.865-.865v-1.297c0-.478-.388-.865-.866-.865zm4.324-4.324h-1.3c-.48 0-.87.38-.87.86v1.29c0 .48.38.86.86.86h1.29c.48 0 .86-.39.86-.87V61.5c0-.48-.39-.864-.87-.864zm-4.33 0h-1.3c-.48 0-.87.38-.87.86v1.29c0 .48.38.86.86.86h1.29c.472 0 .86-.39.86-.87V61.5c0-.48-.39-.864-.867-.864zm12.97 4.32h-1.29c-.48 0-.87.38-.87.86v1.29c0 .48.38.86.86.86h1.29c.48 0 .86-.39.86-.87v-1.29c0-.48-.387-.87-.865-.87zm-4.33 0h-1.29c-.48 0-.87.38-.87.86v1.29c0 .48.38.86.86.86h1.3c.48 0 .862-.39.862-.87v-1.29c0-.48-.39-.87-.867-.87zm4.32-4.33h-1.3c-.48 0-.87.38-.87.86v1.3c0 .48.384.86.862.86h1.3c.476 0 .863-.39.863-.87V61.5c0-.48-.388-.864-.866-.864zm-4.33 0H61.5c-.48 0-.864.38-.864.86v1.3c0 .48.387.86.866.86H62.8c.48 0 .87-.39.87-.87V61.5c0-.48-.383-.864-.86-.864z").attr({
class: "menu-button",
fill: "#9B9B9B",
fillRule: "nonzero"
});
var c1 = s.circle(60, 60, 53).attr({ stroke: "#9B9B9B", transform: "rotate(90 60 60)" });
var c2 = s.circle(60, 7, 2).attr({ fill: "#9B9B9B" });
var c3 = s.circle(60, 113, 2).attr({ fill: "#9B9B9B" });
var c4 = s.circle(113, 60, 2).attr({ fill: "#9B9B9B" });
var c5 = s.circle(7, 60, 2).attr({ fill: "#9B9B9B" });
var outerCircles = s.group(c1, c2, c3, c4, c5).attr({ class: "outer-circle" });
var fullSVG = s.group(bitmojiCircle, bigEyeCircle, L1, smallEyeCircle, opacityCircle, menuButton, outerCircles).attr({ fill: "none", fillRule: "evenodd" });
function OnMouseMove(evt) {
L1.attr({ d: "M "+circleX+" "+circleY +"L "+evt.clientX+" "+evt.clientY });
var totalLength = L1.getTotalLength();
if (totalLength < circleRadius) {
smallEyeCircle.attr({ cx: evt.clientX , cy: evt.clientY });
} else {
var PAL = L1.getPointAtLength(circleRadius);
smallEyeCircle.attr({ cx: PAL.x , cy: PAL.y });
}
}
document.onmousemove = OnMouseMove;
Edit
Tried to throttle/debounce it, as Nikos said, by replacing the OnMouseMove function with the following code:
var pageX = 0,
pageY = 0;
var moveIt = function() {
L1.attr({ d: "M "+circleX+" "+circleY +"L "+pageX+" "+pageY });
var totalLength = L1.getTotalLength();
if (totalLength < circleRadius) {
smallEyeCircle.attr({ cx: pageX, cy: pageY });
} else {
var PAL = L1.getPointAtLength(circleRadius);
smallEyeCircle.attr({ cx: PAL.x , cy: PAL.y });
}
setTimeout(moveIt, 1000/25);
};
$(document).on('mousemove', function(e) {
pageX = e.pageX;
pageY = e.pageY;
}).one('mousemove', moveIt);
This does not seem to work.
Update
I found a better solution, but it's still not 100% functional, the area for the eyeball to move in, is too big, but I don't know how to get it smaller.
Here is the updated fiddle: http://jsfiddle.net/bmp5j4x9/3/
As I've commented you are detecting the mouse position relative to the document and you are using those coordinates to draw inside an SVG canvas whose size is 120/120. This can not work.
Next comes an example (Javascript) where the the line is following the mouse correctly
let m = {}
test.addEventListener("mousemove",(e)=>{
// draw the line on mousemove
m=oMousePosSVG(e);
_line.setAttributeNS(null,"x2",m.x);
_line.setAttributeNS(null,"y2",m.y);
})
function oMousePosSVG(e) {
// a function to detect the mouse position inside an SVG
var p = test.createSVGPoint();
p.x = e.clientX;
p.y = e.clientY;
var ctm = test.getScreenCTM().inverse();
var p = p.matrixTransform(ctm);
return p;
}
<svg id="test" viewBox="0 0 120 120" width="100vw" height="100vh">
<circle cx="60" cy="60" r="20" fill="#d9d9d9" />
<line id="_line" x1="55" y1="60" stroke="blue" />
</svg>
Yet an other solution would be letting things as you have them but recalculating the mouse position in function of the document size:
let w = window.innerWidth;
let h = window.innerHeight;
let m = {}
document.addEventListener("mousemove",(e)=>{
//get the mouse position
m=oMousePos(e);
//calculate the x2 and y2 for the line in function of the size of the window
let x2 = map(m.x, 0, w, 0, 120)
let y2 = map(m.y, 0, h, 0, 120)
// set the attributes x2 and y2 for the line
_line.setAttributeNS(null,"x2",x2);
_line.setAttributeNS(null,"y2",y2);
})
function init(){
// a function to get the size of the window on resize
w = window.innerWidth;
h = window.innerHeight;
}
// you call the init on resize
setTimeout(function() {
init();
addEventListener('resize', init, false);
}, 15);
// a function to get the mouse position
function oMousePos(evt) {
return {
x: evt.clientX,
y: evt.clientY
}
}
function map(n, a, b, _a, _b) {
let d = b - a;
let _d = _b - _a;
let u = _d / d;
return _a + n * u;
}
svg {
border: 1px solid;
position: absolute;
margin: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
<svg id="test" viewBox="0 0 120 120" width="240" >
<circle cx="60" cy="60" r="20" fill="#d9d9d9" />
<line id="_line" x1="55" y1="60" stroke="blue" />
</svg>
I hope it helps.
Update
The OP comments that in fact they want the small red circle to follow the mouse. In this case you need to calculate the angle between the center of the eye and the mouse, and you draw the red circle using this angle:
let m = {}
let c = {x:55,y:60}// the center of the eye
let r = whitecircle.getAttribute("r") - redcircle.getAttribute("r") - .5;
// where .5 is 1/2 stroke-width
test.addEventListener("mousemove",(e)=>{
// draw the line on mousemove
m=oMousePosSVG(e);
//_line.setAttributeNS(null,"x2",m.x);
//_line.setAttributeNS(null,"y2",m.y);
var angle = getAngle(m,c)
//this are the coordinates for the center of the red circle
var x2 = c.x + r * Math.cos(angle);
var y2 = c.y + r * Math.sin(angle);
redcircle.setAttributeNS(null,"cx",x2);
redcircle.setAttributeNS(null,"cy",y2);
})
function oMousePosSVG(e) {
// a function to detect the mouse position inside an SVG
var p = test.createSVGPoint();
p.x = e.clientX;
p.y = e.clientY;
var ctm = test.getScreenCTM().inverse();
var p = p.matrixTransform(ctm);
return p;
}
function getAngle(p1,p2){
// a function to calculate the angle between two points p1 and p2
var deltaX = p1.x - p2.x;
var deltaY = p1.y - p2.y;
return Math.atan2(deltaY, deltaX);
}
<svg id="test" viewBox="0 0 120 120" width="100vw" height="100vh">
<circle cx="60" cy="60" r="20" fill="#d9d9d9" />
<circle id="whitecircle" cx="55" cy="60" r="5" fill="#fff" stroke="black" />
<circle cx="55" cy="60" r="3" fill="#f00" id="redcircle" />
<!--<line id="_line" x1="55" y1="60" stroke="blue" />-->
</svg>

Scale an image to fit when replacing an image

I'm trying to implement a behaviour by which I can add an initial image to a canvas (placeholder) and then subsequently I can then choose a different image to replace that image - that new image should then scale itself to "fit" within the bounds of the old image. For example if I choose a portrait image as the "placeholder" and then choose a landscape image to replace it, Id expect the landscape image to first scale its width and maintain aspect ratio, and then vertically align itself within the boundaries of the placeholder dimensions.
Here is a jsfiddle of what I have working so far: https://jsfiddle.net/cgallagher/co8dg527/1/
and here is the code:
var canvas = new fabric.Canvas('stage');
var cw = canvas.getWidth()
var ch = canvas.getHeight()
var _currentImage;
function setProductImage(url){
var oldWidth;
var oldHeight;
var oldLeft;
var oldTop;
fabric.Image.fromURL(url, function(img) {
if (_currentImage) {
oldWidth = _currentImage.getScaledWidth()
oldHeight = _currentImage.getScaledHeight()
oldLeft = _currentImage.left
oldTop = _currentImage.top
canvas.remove(_currentImage);
}
_currentImage = img;
img.set({ 'left': oldLeft || 0 })
img.set({ 'top': oldTop || 0 })
if (oldHeight && oldHeight > img.getScaledHeight()){
img.scaleToWidth(oldWidth);
img.set({'height': oldHeight })
} else if (oldWidth > img.getScaledWidth()){
img.scaleToHeight(oldHeight);
img.set({'width': oldWidth })
}
img.selectable = true
canvas.add(img).setActiveObject(img);
});
}
function addImage(url){
setProductImage(url)
canvas.renderAll()
}
You can see that the image does scale the way I'd like but it doesn't then align itself.
I'm toying with the idea of dropping a bounding box around the image too and possibly trying to align and scale within that but I'm not sure this is even possible with fabric?
This is what I had done.
When you run scaleToWidth and scaleToHeight the scaleX andscaleY is changing and you need to adjust the oldHeight and oldWidth to the new img.scaleX/img.scalaY
I also rewrite _renderFill method from fabric.Image.prototype to create that offset effect.
Check here:https://jsfiddle.net/mariusturcu93/co8dg527/37/
Code
<html>
<head>
<title>Fabric kicking</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.0/fabric.js"></script>
<style>
#stage {
border: solid 1px #333;
}
</style>
</head>
<body>
<button onclick="addImage('https://s.hs-data.com/bilder/spieler/gross/208995.jpg')">Add image</button>
<button onclick="addImage('https://cdn.images.express.co.uk/img/dynamic/67/590x/Mateusz-Klich-883903.jpg')">add another image</button>
<canvas id="stage" width="600" height="600"></canvas>
<script>
var canvas = new fabric.Canvas('stage');
var cw = canvas.getWidth()
var ch = canvas.getHeight()
var _currentImage;
function setProductImage(url){
var oldWidth;
var oldHeight;
var oldLeft;
var oldTop;
fabric.Image.fromURL(url, function(img) {
if (_currentImage) {
oldWidth = _currentImage.getScaledWidth()
oldHeight = _currentImage.getScaledHeight()
oldLeft = _currentImage.left
oldTop = _currentImage.top
canvas.remove(_currentImage);
}
_currentImage = img;
img.set({ 'left': oldLeft || 0 })
img.set({ 'top': oldTop || 0 })
if (oldHeight && oldHeight > img.getScaledHeight()){
img.scaleToWidth(oldWidth);
img.set({'height': oldHeight/img.scaleX})
} else if (oldWidth > img.getScaledWidth()){
img.scaleToHeight(oldHeight);
img.set({'width': oldWidth/img.scaleY })
}
img.selectable = true
canvas.add(img).setActiveObject(img);
});
}
function addImage(url){
setProductImage(url)
canvas.renderAll()
}
</script>
<img src="https://s.hs-data.com/bilder/spieler/gross/208995.jpg" />
<img src="https://cdn.images.express.co.uk/img/dynamic/67/590x/Mateusz-Klich-883903.jpg" />
</body>
</html>
fabric.Image.prototype._renderFill rewrite
fabric.Image.prototype._renderFill= (function(renderFill){
return function(ctx) {
var w = this.width, h = this.height, sW = w * this._filterScalingX, sH = h * this._filterScalingY,
x = -w / 2, y = -h / 2, elementToDraw = this._element;
y = y + Math.abs((this._element.height-h)/2);
x = x + Math.abs((this._element.width-w)/2);
elementToDraw && ctx.drawImage(elementToDraw,
this.cropX * this._filterScalingX,
this.cropY * this._filterScalingY,
sW,
sH,
x, y, w, h);
}
})()

How to animate element along svg path on scroll?

I have an issue: I have to change element position&angle on scroll event. Like my webpage is a long road with background - and I need to animate car movements along this path during the scroll.
I good explanation is here: http://prinzhorn.github.io/skrollr-path/ - a perfect solution, that meets my requirements. But Unfortunately it's extremely outdated.
Maybe somebody has an up to date solution-library? Or code-ideas, how to animate element via svg-path with page scrolling?
Also i tried http://scrollmagic.io/examples/expert/bezier_path_animation.html - but it's not something that I need, because my path is difficult. Not just a couple of circles.
Here is some vanilla Javascript that moves a "car" along a path according to how much the page has scrolled.
It should work in all (most) browsers. The part that you may need to tweak is how we get the page height (document.documentElement.scrollHeight). You may need to use different methods depending on the browsers you want to support.
function positionCar()
{
var scrollY = window.scrollY || window.pageYOffset;
var maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
var path = document.getElementById("path1");
// Calculate distance along the path the car should be for the current scroll amount
var pathLen = path.getTotalLength();
var dist = pathLen * scrollY / maxScrollY;
var pos = path.getPointAtLength(dist);
// Calculate position a little ahead of the car (or behind if we are at the end), so we can calculate car angle
if (dist + 1 <= pathLen) {
var posAhead = path.getPointAtLength(dist + 1);
var angle = Math.atan2(posAhead.y - pos.y, posAhead.x - pos.x);
} else {
var posBehind = path.getPointAtLength(dist - 1);
var angle = Math.atan2(pos.y - posBehind.y, pos.x - posBehind.x);
}
// Position the car at "pos" totated by "angle"
var car = document.getElementById("car");
car.setAttribute("transform", "translate(" + pos.x + "," + pos.y + ") rotate(" + rad2deg(angle) + ")");
}
function rad2deg(rad) {
return 180 * rad / Math.PI;
}
// Reposition car whenever there is a scroll event
window.addEventListener("scroll", positionCar);
// Position the car initially
positionCar();
body {
min-height: 3000px;
}
svg {
position: fixed;
}
<svg width="500" height="500"
viewBox="0 0 672.474 933.78125">
<g transform="translate(-54.340447,-64.21875)" id="layer1">
<path d="m 60.609153,64.432994 c 0,0 -34.345187,72.730986 64.649767,101.015256 98.99494,28.28427 321.2285,-62.62946 321.2285,-62.62946 0,0 131.31984,-52.527932 181.82746,16.16244 50.50763,68.69037 82.04198,196.41856 44.44671,284.86302 -30.25843,71.18422 -74.75128,129.29952 -189.90867,133.34013 -115.15739,4.04061 -72.73099,-153.54318 -72.73099,-153.54318 0,0 42.42641,-129.29953 135.36044,-119.198 92.93404,10.10152 -14.14213,-129.29953 -141.42135,-94.95434 -127.27922,34.34518 -183.84777,80.8122 -206.07112,121.2183 -22.22336,40.40611 -42.06243,226.23742 -26.26397,305.06607 8.77013,43.75982 58.20627,196.1403 171.72594,270.72088 73.8225,48.50019 181.82745,2.02031 181.82745,2.02031 0,0 94.95434,-12.12183 78.7919,-155.56349 -16.16244,-143.44166 -111.68403,-138.77778 -139.9683,-138.77778 -28.28427,0 83.39976,-156.18677 83.39976,-156.18677 0,0 127.27922,-189.90867 107.07617,16.16245 C 634.3758,640.21994 864.69058,888.71747 591.94939,941.2454 319.2082,993.77334 -16.162441,539.20469 153.54319,997.81395"
id="path1"
style="fill:none;stroke:#ff0000;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path id="car" d="M-15,-10 L15,0 L -15,10 z" fill="yellow" stroke="red" stroke-width="7.06"/>
</g>
</svg>
I adapted Paul LeBau's answer for my purposes in TypeScript and figured I'd leave it here in case it's helpful for anyone.
NOTE: It's broken as hell when running the snippet in Stack Overflow. No clue why when it functions perfectly in VSCode. First thing I'd recommended is full screening the snippet if you can't see the SVG. Then actually try copy/pasting this into your own project before making any claims about there being an issue with the posted code.
Notable additions from Paul's:
A clickToScroll function allowing you to click anywhere on the svg path and have it scroll accordingly (does not work at all in the SO snippet window)
The car (rider) will face towards the direction it's moving
Here's the Typescript version separately as SO doesn't support TS :(
interface PathRider {
ride: () => void;
clickToScroll: (e: MouseEvent) => void;
onClick: (e: MouseEvent, callback: (pt: DOMPoint) => void) => void;
};
const usePathRider = (
rider: SVGPathElement,
path: SVGPathElement,
rideOnInit = true
): PathRider => {
const maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
const pathLen = path.getTotalLength();
//====================
/* Helper Functions */
const radToDeg = (rad: number) => (180 * rad) / Math.PI;
const distance = (a: DOMPoint, b: DOMPoint) =>
Math.sqrt(Math.pow(a.y - b.y, 2) + Math.pow(a.x - b.x, 2));
//=========================
/* Click-based Functions */
const step = 0.5; // how granularly it should check for the point on the path closest to where the user clicks. the lower the value the less performant the operation is
let currLen = -step;
const pointArr: DOMPoint[] = [];
while ((currLen += step) <= pathLen)
pointArr.push(path.getPointAtLength(currLen));
const onClick = (e: MouseEvent, callback: (pt: DOMPoint) => void) => {
let pt = new DOMPoint(e.clientX, e.clientY);
callback(pt.matrixTransform(path.getScreenCTM().inverse()));
};
const getLengthAtPoint = (pt: DOMPoint) => {
let bestGuessIdx = 0;
let bestGuessDist = Number.MAX_VALUE;
let guessDist: number;
pointArr.forEach((point, idx) => {
if ((guessDist = distance(pt, point)) < bestGuessDist) {
bestGuessDist = guessDist;
bestGuessIdx = idx;
}
});
return bestGuessIdx * step;
};
const getScrollPosFromLength = (len: number) => (len * maxScrollY) / pathLen;
const clickToScroll = (e: MouseEvent) => {
onClick(e, (point) => {
const lengthAtPoint = getLengthAtPoint(point);
const scrollPos = getScrollPosFromLength(lengthAtPoint);
window.scrollTo({
top: scrollPos,
behavior: 'smooth',
});
});
};
//==========================
/* Scroll-based functions */
let lastDist: number; // for determining direction
const ride = () => {
const scrollY = window.scrollY || window.pageYOffset;
const dist = (pathLen * scrollY) / maxScrollY;
const pos = path.getPointAtLength(dist);
let angle: number;
// calculate position a little ahead of the rider (or behind if we are at the end),
// so we can calculate the rider angle
const dir = lastDist < dist; // true=right
if (dir ? dist + 1 <= pathLen : dist - 1 >= 0) {
const nextPos = path.getPointAtLength(dist + (dir ? 1 : -1));
angle = Math.atan2(nextPos.y - pos.y, nextPos.x - pos.x);
} else {
const nextPos = path.getPointAtLength(dist + (dir ? -1 : 1));
angle = Math.atan2(pos.y - nextPos.y, pos.x - nextPos.x);
}
lastDist = dist;
rider.setAttribute(
'transform',
`translate(${pos.x}, ${pos.y}) rotate(${radToDeg(angle)})`
);
};
if (rideOnInit) ride();
return {
ride,
clickToScroll,
onClick,
};
};
Snippets for running in SO
const usePathRider = (
rider,
path,
rideOnInit = true
) => {
const maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
const pathLen = path.getTotalLength();
const step = 0.5;
let currLen = -step;
const pointArr = [];
while ((currLen += step) <= pathLen)
pointArr.push(path.getPointAtLength(currLen));
//====================
/* Helper Functions */
const radToDeg = (rad) => (180 * rad) / Math.PI;
const distance = (a, b) =>
Math.sqrt(Math.pow(a.y - b.y, 2) + Math.pow(a.x - b.x, 2));
//============
/* Closures */
const onClick = (e, callback) => {
let pt = new DOMPoint(e.clientX, e.clientY);
callback(pt.matrixTransform(path.getScreenCTM().inverse()));
};
const getLengthAtPoint = (pt) => {
let bestGuessIdx = 0;
let bestGuessDist = Number.MAX_VALUE;
let guessDist;
pointArr.forEach((point, idx) => {
if ((guessDist = distance(pt, point)) < bestGuessDist) {
bestGuessDist = guessDist;
bestGuessIdx = idx;
}
});
return bestGuessIdx * step;
};
const getScrollPosFromLength = (len) => (len * maxScrollY) / pathLen;
const clickToScroll = (e) => {
onClick(e, (point) => {
const lengthAtPoint = getLengthAtPoint(point);
const scrollPos = getScrollPosFromLength(lengthAtPoint);
window.scrollTo({
top: scrollPos,
behavior: 'smooth',
});
});
};
let lastDist;
const ride = () => {
const scrollY = window.scrollY || window.pageYOffset;
const dist = (pathLen * scrollY) / maxScrollY;
const pos = path.getPointAtLength(dist);
let angle;
// calculate position a little ahead of the rider (or behind if we are at the end),
// so we can calculate the rider angle
const dir = lastDist < dist; // true=right
if (dir ? dist + 1 <= pathLen : dist - 1 >= 0) {
const nextPos = path.getPointAtLength(dist + (dir ? 1 : -1));
angle = Math.atan2(nextPos.y - pos.y, nextPos.x - pos.x);
} else {
const nextPos = path.getPointAtLength(dist + (dir ? -1 : 1));
angle = Math.atan2(pos.y - nextPos.y, pos.x - nextPos.x);
}
lastDist = dist;
rider.setAttribute(
'transform',
`translate(${pos.x}, ${pos.y}) rotate(${radToDeg(angle)})`
);
};
if (rideOnInit) ride();
return {
ride,
clickToScroll,
onClick,
};
};
/* VANILLA JS USAGE */
const svgRider = document.getElementById('rider');
const svgPath = document.getElementById('path');
const pathRider = usePathRider(svgRider, svgPath);
// Reposition car whenever there is a scroll event
window.addEventListener("scroll", pathRider.ride);
body {
min-height: 3000px;
}
svg {
position: fixed;
}
<svg onclick="pathRider.clickToScroll" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 300 285" shape-rendering="geometricPrecision" text-rendering="geometricPrecision"><path
id="path"
d="M22.061031,163.581791c.750375-2.251126,2.251125-16.508254,26.263131-20.26013s20.26013-8.254128,42.02101,2.251125q21.76088,10.505253-12.006004,18.009005-33.016508.750374,0-18.009005t52.526263,4.427213q6.003001,19.584792,19.134567,14.332166t16.133067-14.332166q32.266133-12.081041,45.772886,0t47.273637,0"
transform="translate(.000002 0.000001)"
fill="none"
stroke="#3f5787"
stroke-width="0.6"
stroke-dasharray="3"
/>
<path
id="rider"
d="M-2,-2 L3,0 L -2,2 z"
stroke="red"
stroke-width="0.6"
/></svg
>

How can I make my cursor synced with the control point during a resize?

I am creating an editor.
I would like the basic functions on my objects which are rotate/resize and translate.
I've managed to do the three of them but the only problem is now my mouse position doesn't follow the control points (the problem gets even worst for the other control points not shown below)..
You'll find below an example for the right middle resize with a rotation angle of 30 degrees with no mouseY position, note that the mouse follows perfectly my control point when the rotation angle equals 0.
Is there a way easily solve this problem, am I going the wrong way?
Here's the jsfiddle link where you can change the rotate angle in the code to see by yourself JSiddle link. (Just click and drag the black control point to resize the object)
//convert value of range amin to amax to the range bmin to bmax;
function imap(value, amin, amax, bmin, bmax)
{
if ((amax - amin))
return (value - amin) * (bmax - bmin) / (amax - amin) + bmin;
return (0);
};
//get mouse coordinates from the SVG element
function getMouse(el, e)
{
var pt = el.createSVGPoint();
pt.x = e.clientX;
pt.y = e.clientY;
var cursorpt = pt.matrixTransform(el.getScreenCTM().inverse());
return({x: cursorpt.x, y: cursorpt.y})
};
var controlPoint = document.getElementById("c"); //My control point element
var mouseX;
var mouseXClicked = 0;
var scaleX = 1;
var scaleY = 1;
var scaleXClicked = 1;
var control = false; // sets if resizeRightMiddle() should be executed
var rectWidth = 100; //is normally tooken with a getBBox() function
var scale = document.getElementById("scale");
function resizeRightMiddle()
{
//convert difference between original mouse X postion on click and actual X mouse position into a scale factor
plusX = imap(mouseX - mouseXClicked, 0, rectWidth, 0, 1);
//add converted scale factor to the original x scale value
resX = scaleXClicked + plusX;
scale.setAttribute('transform', 'scale(' + resX + ',' + scaleY + ')');
scaleX = resX;
}
var svg = document.getElementById("main");
// save Scale and X mouse coordinate on click
svg.addEventListener("mousedown", function(e){
var coord = getMouse(svg, e);
mouseXClicked = coord.x;
scaleXClicked = scaleX;
});
svg.addEventListener("mousemove", function(e){
//get mouse coordinates
var coord = getMouse(svg, e);
mouseX = coord.x;
// resize if control element has been clicked
if (control)
resizeRightMiddle();
});
// desactivate resize
svg.addEventListener("mouseup", function(e){
control = false;
});
//activate resize
controlPoint.addEventListener("mousedown", function(){
control = true;
});
svg {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
<div>
<svg id="main" width="1000" height="300">
<g transform="translate(66, 56)">
<g id="rotate" transform-origin="center" transform="rotate(30)">
<g id="scale">
<path fill="red" stroke="red" d="M 0 0 L 0 100 L 100 100 L 100 0Z" />
<rect id="c" fill="black" stroke="black" x=95 y=45 width=10 height=10 />
</g>
</g>
</g>
</svg>
</div>
The code below calculates how much the mouse moves in the direction of the rectangle's orientation on each mousemove event, instead of from the beginning of the mousedown to the current mousemove. It then updates updatedRectWidth and uses that to calculate the current desired scale.
var controlPoint = document.getElementById("c");
var control = false;
var origRectWidth = 100;
var scale = document.getElementById("scale");
var relevantMouseMoveDist = 0;
var updatedRectWidth = origRectWidth;
var mouseDownPt = {};
var rotateDiv = document.getElementById("rotate");
var rotateString = rotateDiv.getAttribute('transform');
var rectangleAngle = parseInt(rotateString.slice(rotateString.indexOf("(") + 1)) * Math.PI / 180; // retrieve the angle from the DOM
var relevantMouseMoveDist;
var newMousePosn;
var oldMousePosn;
function resizeRightMiddle()
{
updatedRectWidth += relevantMouseMoveDist;
xScale = updatedRectWidth/origRectWidth;
scale.setAttribute('transform', 'scale(' + xScale + ',1)');
}
var svg = document.getElementById("main");
svg.addEventListener("mousemove", function(e){
if (newMousePosn) {
// the former mouse pos'n
oldMousePosn = {x: newMousePosn.x, y: newMousePosn.y};
// the new mouse pos'n
newMousePosn = {x: e.clientX, y: e.clientY};
// the change in the mouse pos'n coordinates since the last move event
var deltaMouseMove = {
x: newMousePosn.x - oldMousePosn.x,
y: newMousePosn.y - oldMousePosn.y
};
// the dir'n of this movement
var angleOfMouseMovement = Math.atan2(deltaMouseMove.y, deltaMouseMove.x);
// the absolute distance the mouse has moved
var mouseMoveDist = Math.sqrt(
deltaMouseMove.x * deltaMouseMove.x +
deltaMouseMove.y * deltaMouseMove.y
);
// the difference in direction between the mouse movement and orientation of the rectangle
var angleDifference = angleOfMouseMovement - rectangleAngle;
// the portion of the mouse movement that is in the direction of the rectangle's orientation
relevantMouseMoveDist = mouseMoveDist * Math.cos(angleDifference);
// resize the rectangle if necessary
if (control) resizeRightMiddle();
} else {
// establish the mouse pos'n during the first mousemove event
newMousePosn = {x: e.clientX, y: e.clientY};
}
});
svg .addEventListener("mouseup" , function(e){control = false;});
controlPoint.addEventListener("mousedown", function(e){control = true ;});
<div>
<svg id="main" width="1000" height="300">
<g transform="translate(66, 56)">
<g id="rotate" transform-origin="center" transform="rotate(40)">
<g id="scale">
<path fill="red" stroke="red" d="M 0 0 L 0 100 L 100 100 L 100 0Z" />
<rect id="c" fill="black" stroke="black" x=95 y=45 width=10 height=10 />
</g>
</g>
</g>
</svg>
</div>

Categories

Resources