Reputation: 409
I have A SVG with two eyes where each pupil tracks the mouse position.
The issue is, the two eyes acts individually and not as a group. For example if you position the cursor between both eyes, the pupil from the left goes in the right direction and the pupil from the right goes to the left direction.
What I would like to achieve is, when the cursor is positioned between (or close to) both eyes, the pupils stay in the center of the eye.
let l1 = document.querySelector("#l1");
let l2 = document.querySelector("#l2");
let svg1 = document.querySelector("#svg1");
const toSVGPoint = (svg, x, y) => {
let p = new DOMPoint(x, y);
return p.matrixTransform(svg.getScreenCTM().inverse());
};
document.addEventListener('mousemove', e => {
let p = toSVGPoint(svg1, e.clientX, e.clientY);
l1.setAttribute('x2', p.x);
l1.setAttribute('y2', p.y);
l2.setAttribute('x2', p.x);
l2.setAttribute('y2', p.y);
});
.container{
width: 20em;
height: 20em;
margin: 200px auto;
}
<div class="container">
<svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<ellipse cx="80" cy="45" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
<ellipse cx="120" cy="45" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
<line marker-start="url(#pupil)" id="l1" x1="80" y1="45" />
<line marker-start="url(#pupil)" id="l2" x1="120" y1="45" />
<defs>
<marker id="pupil" viewBox="0 0 18 18" refX="10" refY="5" markerWidth="37" markerHeight="37" orient="auto-start-reverse">
<circle fill="#fdd176" r="4" cy="5" cx="5" />
</marker>
</defs>
</svg>
</div>
Here is my codepen where you can see it live.
And another codepen where you can see the wanted result.
Thank you.
Upvotes: 1
Views: 82
Reputation: 16666
The following solution initially places the pupils in the center of the eye and then translates them by one tenth of the distance that the mouse pointer has from the center of the SVG viewBox (which I chose to be (0, 0) for simplicity). The factor of one tenth is chosen so that when the mouse reaches the edge of the viewBox, the pupils reach the edge of the eye. Mouse movements beyond the edge of the viewBox are ignored.
Both pupils always move in parallel, like in the "wanted result" codepen.
const toSVGPoint = (svg, x, y) => {
let p = new DOMPoint(x, y).matrixTransform(svg.getScreenCTM().inverse());
if (p.x < -100) p.x = -100;
else if (p.x > 100) p.x = 100;
if (p.y < -100) p.y = -100;
else if (p.y > 100) p.y = 100;
return p;
};
document.addEventListener('mousemove', e => {
let p = toSVGPoint(svg1, e.clientX, e.clientY);
g.setAttribute("transform", `translate(${p.x/10} ${p.y/10})`);
});
svg {
margin: 5pc;
}
ellipse {
stroke: #fdd176;
stroke-width: 10;
}
circle {
fill: #fdd176;
}
<svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewBox="-100 -100 200 200">
<ellipse cx="-20" cy="0" rx="20" ry="22"/>
<ellipse cx="20" cy="0" rx="20" ry="22"/>
<g id="g">
<circle r="6" cy="0" cx="-20" />
<circle r="6" cy="0" cx="20" />
</g>
</svg>
Upvotes: 2
Reputation: 13040
With the technique, using the a marker, you can adjust the refX
of the marker element. As a default value it is 8, but when you point in between the eyes the value need to be smaller.
This answer has been updated:
I wasn't satisfied with my original calculation (and it was also commented), so I did two things: The focus point is not the same for both eyes. So now, the eyes do not squint. The points are 20 to the left of the mouse for the left eye, and equally for the right eye, to the right. Second I did a refactoring of the calculation that calculates the refX
attribute.
let l1 = document.querySelector("#l1");
let l2 = document.querySelector("#l2");
let svg1 = document.querySelector("#svg1");
const toSVGPoint = (svg, x, y) => {
let p = new DOMPoint(x, y);
return p.matrixTransform(svg.getScreenCTM().inverse());
};
document.addEventListener('mousemove', e => {
let p = toSVGPoint(svg1, e.clientX, e.clientY);
l1.setAttribute('x2', p.x - 20);
l1.setAttribute('y2', p.y);
l2.setAttribute('x2', p.x + 20);
l2.setAttribute('y2', p.y);
let refx = 8;
if(p.x > l1.x1.baseVal.value && p.x < l2.x1.baseVal.value){
let distY = Math.abs(l1.y1.baseVal.value - p.y);
if(distY < 20) refx = 5 + distY/20 * 3;
}
document.querySelector("#pupil").setAttribute('refX', refx);
});
.container {
width: 20em;
height: 20em;
margin: 200px auto;
}
<div class="container">
<svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<ellipse cx="80" cy="45" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
<ellipse cx="120" cy="45" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
<line marker-start="url(#pupil)" id="l1" x1="80" y1="45" />
<line marker-start="url(#pupil)" id="l2" x1="120" y1="45" />
<defs>
<marker id="pupil" viewBox="0 0 18 18" refX="8" refY="5" markerWidth="37" markerHeight="37" orient="auto-start-reverse">
<circle fill="#fdd176" r="4" cy="5" cx="5" />
</marker>
</defs>
</svg>
</div>
Upvotes: 2
Reputation: 21143
Enhancement of Heiko his answer
<moving-eyes></moving-eyes>
<moving-eyes color="lightblue"></moving-eyes>
<moving-eyes color="sandybrown"></moving-eyes>
<script>
customElements.define("moving-eyes", class extends HTMLElement {
connectedCallback() {
let color = this.getAttribute("color") || "#fdd176";
let id = crypto.randomUUID(); // every SVG needs unique IDs
this.innerHTML = `
<svg viewBox="-100 -100 200 200" style="height:150px;background:beige">
<ellipse id="eye${id}" cx="-20" cy="0" rx="20" ry="22" stroke-width="10" stroke="${color}"/>
<use href="#eye${id}" x="40"/>
<g>
<circle id="pupil${id}" fill="${color}" r="6" cy="0" cx="-20" />
<use href="#pupil${id}" x="40" />
</g>
</svg>`;
this.svg = this.querySelector("svg");
this.svg.addEventListener('mousemove', e => this.mousemove(e));
}
mousemove(e) {
let p = (new DOMPoint(e.clientX, e.clientY)).matrixTransform(this.svg.getScreenCTM().inverse());
this.querySelector("g").setAttribute("transform", `translate(${p.x/10}, ${p.y/10})`);
}
disconnectedCallback() {
removeEventListener("mousemove", this.mousemove);
}
});
</script>
Upvotes: 1