Touch events are wrong for transformed SVG elements - svg

In the process of experimenting with scaling/panning an inline SVG image by applying a matrix transform I have discovered a rather peculiar thing. After loading the image I am attaching a touchstart event listener to some of the elements in the SVG image and the event fires right away when the object is touched. However, after applying a transform
document.getElementById('mysvg').setAttribute('transform''matrix(a b c d e)')
which has the effect of scaling and/or translating the entire SVG image touching the same object no longer triggered the expected touch event. After some experiment I found that the event could still be triggered by the touch location on screen had no bearing to the actual new placement of the object on the screen. I then proceeded to first removeEventListener followed by addEventListener for the object after issuing the matrix transform and lo & behold the touch event handling was back to normal.
Quite apart from the fact that I would like to avoid the rather expensive operations of removing & then reassigning the same event listener after each pan/zoom I would like to understand just why this is happening. It is like the browser is locating the pixel location of the object at the addEventListener stage and then holds on to that somewhere in its memory blissfully ignorant of any object displacements that might have occurred later.
Can anyone here tell me what is going on here and how I can go about retaining the utility of the touch event after pan & zoom in a more efficient manner?

I've set up a similar issue:
There is a <circle> element, with a transform attribute inside an <svg>.
The 'touchstart' event fires only at the first tap on the <circle>. After that it doesn't trigger the 'touchstart' event anymore.
I have found a strange workaround: Add a 'touchstart' eventListener to the <svg> element with a noop handler:
document.querySelector('svg').addEventListener('touchstart', () => {});
After this the <circle> triggers the 'touchstart' events perfectly.
You can test it with the folllowing snipet:
let debugLines = [];
let lines = 0;
function writeDebug(...t) {
let d = document.getElementById('debug');
debugLines.unshift(`${lines++}: ${t.join(' ')}`);
debugLines.splice(5);
d.innerHTML = debugLines.join('<br />');
}
document.querySelectorAll('circle')[0].addEventListener('touchstart', writeDebug);
/* remove comment from the line below to test workaround */
// document.querySelector('svg').addEventListener('touchstart', () => {});
<!doctype html>
<html>
<head>
<style>
svg { background: #f0f0f0; width: 200px; float: left; }
</style>
</head>
<body>
<div id="wrap">
<svg viewBox="-50, -50, 100, 100" class="b-circular-slider-svg">
<circle cx="0" cy="0" r="8"
stroke="#ccc" fill="#fafafa"
transform="translate(0, -10)"></circle>
</svg>
</div>
<strong>debug:</strong>
<div id="debug"></div>
</body>
</html>

Related

Can a SVGStyleElement be disabled like an HTMLStyleElement can?

Short version:
Unlike the HTMLStyleElement the SVGStyleElement has no disabled property (according to the MDN docs)
Question: can a whole SVGStyleElement be disabled/toggled?
Longer playground version:
I can inject HTML in a DIV and the HTMLStyleElement remains a HTMLStyleElement and respects the disabled state.
(there is a FOUC because unload=this.disabled=true kicks in after parsing)
The disabled Toggle Button works as it should.
Injected in an SVG the HTMLStyleElement becomes a SVGStyleElement
The onload is ignored (the exact behaviour I want for my Web Component)
FireFox shows the state undefined on first load because there is no disabled property
but!! the disabled Toggle button works in Chromium, not in FireFox (and Safari??)
Question: Which Browsers are doing what right?
<template id=TEMPLATE>
<style onload="this.disabled=true">
body { background: red; }
circle { fill:green; }
</style>
<circle cx="50%" cy="50%" r="45%"></circle>
</template>
<div id=DIV></div>
<svg id=SVG width="40" height="40"></svg>
<script>
let STYLE;// global
function inject(id) {
DIV.innerHTML=``;SVG.innerHTML=``; // wipe all
INNER.innerHTML=``;DISABLEDSTATE.innerHTML=``;
id.innerHTML = TEMPLATE.innerHTML; // set innerHTML in DIV or SVG
INNER.append(TEMPLATE.innerHTML); // show template HTML as text
setTimeout(() => { // wait till DOM is fully updated
STYLE = id.querySelector("style"); // get style tag in DIV or SVG
DISABLEDSTATE.append(STYLE.constructor.name,' - disabled state: ',STYLE.disabled);
})
}
function toggle(){
STYLE.disabled = !STYLE.disabled;
DISABLEDSTATE.innerHTML =``;
DISABLEDSTATE.append(STYLE.constructor.name,' - disabled state: ',STYLE.disabled);
}
</script>
<hr>
<button onclick="inject(DIV)">inject Template in DIV</button>
<button onclick="toggle()">toggle STYLE disabled state</button>
<button onclick="inject(SVG)">inject Template in SVG</button>
<hr><b>Injected HTML:</b><div id=INNER></div>
<b>Style disabled state:</b><div id=DISABLEDSTATE></div>
Both elements support the media property, so you can use the media="not all" hack found here: https://stackoverflow.com/a/59572781/1869660
Edit: Looks like SVGStyleElement.media is read-only in Chrome, so you need to start with a normal (enabled) style sheet and use both techniques:
<template id=TEMPLATE>
<style>
body { background: red; }
circle { fill:green; }
</style>
<circle cx="50%" cy="50%" r="45%"></circle>
</template>
function toggle() {
if ((STYLE.media === 'all') || !STYLE.media) {
STYLE.media = 'not all'; //Firefox
STYLE.disabled = true; //Chrome
}
else {
STYLE.media = 'all'; //Firefox
STYLE.disabled = false; //Chrome
}
DISABLEDSTATE.innerHTML = ``;
DISABLEDSTATE.append(STYLE.constructor.name, ' state: ', STYLE.media);
}
https://jsfiddle.net/mvb9fkay/
Chrome has implemented disabled on SVGStyleElement for some time now.
The SVG 2 specification says this about the style element
SVG 2 ‘style’ element shall be aligned with the HTML5 ‘style’ element.
In recognition of that the SVGStyleElement has recently had a disabled property added to it.
Additionally I've landed a patch to Firefox that implements disabled on SVGStyleElements. That should be available from Firefox 104 but you can already try it out in nightlies if you wish.

How to stop a Velocity animation using "promise-style"

I'm using Velocity with promise (Without jQuery...):
var Velocity = require('velocity-animate');
var animation = Velocity(document.getElementById('some-id'), { opacity: 0 });
How can I arbitrary stop this animation since animation returns a promise ?
You can stop any running animation on a dom element using utility function, all you need to do is pass dom element as first parameter and "stop" as second parameter, Velocity(document.getElementById('some-id'),"stop").
See this working example
var a = Velocity(document.getElementById('rect'),{opacity:0},3000);
setTimeout(function(){
Velocity(document.getElementById('rect'),"stop");
},1000);
#rect{
background-color:green;
height:100px;
width:100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<div id="rect">
</div>
As you can see promise a is not used to stop animation.

Scaling a circle in SVG

I have the following SVG code. To scale the circle only, I have to scale the container. I can't scale the circle inside the SVG.
I can access the circle element, but why can't I like other html elements? Is the code scaling the circle or the container?
<html>
<head>
<script>
function scale() {
document.getElementById('container').setAttribute('currentScale', 1.5);
}
</script>
</head>
<body>
<svg height="150" width="150" id="container">
<circle cx="25" r="20" cy="20" fill="blue" id="circle"></circle>
</svg>
<button id="zoom" onclick="scale()">scale</button>
</body>
</html>
I've never tried scaling the whole SVG (using the pure SVG transform attribute). But look like you can just scale each element inside SVG. In this case you have to target (select) the circle first before calling setAttribute on it to modify the transform attribute to scale(1.5,1.5) like this:
function scale() {
document.querySelector("#container > circle")
.setAttribute('transform', 'scale(1.5,1.5)');
}
Here is the Demo. Note that you have to select the option No Wrap - in <head> (on the right hand in the Frameworks & Extensions section). Or better you should attach click handler right in JS code editor (not inline as an attribute in HTML code).
Here is one way to scale just the circle using JS:
Demo: http://jsfiddle.net/hb4nnau0/1/
For convenience, give your circle a transform attribute with no change in scale added. (Alternatively you can add this programmatically on demand.)
`
In your event handler, modify the scale of this transform element:
var circle = document.querySelector('circle'); // Or however, e.g. by id
var scale = circle.transform.animVal.getItem(0); // The first transform
scale.setScale(4,3); // Modify the transform

Google Earth Plugin, embedded in Blogger, producing 2 maps

Curious problem whilst embedding a Google Earth network link into Blogger.
The code I'm using is as shown below, but I'm getting two instances of GE on the same page, one above the other.
They must be getting generated seperately, as if I stick a border into the divs style on the page it only affects one instance.
<div id="map3d" style="border: 4px solid silver; height: 768px; width: 1024px;"></div>
However, if I remove this code from the page entirely. both instances vanish.
Other than that I've got it functioning as I'm wanting. (Eventually)
This is the code I've got in the head section
<!-- Earth -->
<script src="//www.google.com/jsapi?key=mykey"></script>
<script type="text/javascript">
var ge;
google.load("earth", "1", {"other_params":"sensor=false"});
function init() {
google.earth.createInstance('map3d', initCB, failureCB);
}
function initCB(instance) {
ge = instance;
ge.getWindow().setVisibility(true);
ge.getNavigationControl().setVisibility(ge.VISIBILITY_SHOW);
var href = 'http://urltomykmz';
google.earth.fetchKml(ge, href, function(kmlObject) {
if (kmlObject)
ge.getFeatures().appendChild(kmlObject);
if (kmlObject.getAbstractView() !== null)
ge.getView().setAbstractView(kmlObject.getAbstractView());
});
}
function failureCB(errorCode) {
}
google.setOnLoadCallback(init);
</script>
<!-- Earth -->
Grateful to anyone who can point to what's causing the second instance. Thanks.
You have to remove either the call to init() in your onload handler or the call to google.setOnLoadCallback(init), otherwise a map will be added every time you call the init function.

SVG shapes added to Raphael group fire events as if separate

Imagine this:
2 concentric circles, with the smaller one over the larger one so
that both are visible
both are added to a Raphael group (set)
the group has mouseout and mouseover event handlers
Problem:
When the cursor goes from one circle to the other, both event handlers fire, as if they were added separately to each circle.
What I want is for events to be handled for the entire group as if it was a single shape.
How can I achieve that?
Here's the html code:
<!DOCTYPE html>
<html>
<head>
<script src="http://cdnjs.cloudflare.com/ajax/libs/raphael/2.0.0/raphael-min.js"></script>
<meta charset=utf-8 />
<title>JS Bin</title>
</head>
<body>
<div id="target"></div>
</body>
</html>
Javascript:
var W=200;
var H=200;
var paper=new Raphael(document.getElementById('target'),W,H);
var c1=paper.circle(W/2,H/2,70).attr({fill:'orange','stroke-width':4,stroke:'red',opacity:0.7});
var c2=paper.circle(W/2,H/2,50).attr({fill:'green','stroke-width':4,stroke:'yellow',opacity:0.7});
var group=paper.set();
group.push(c1,c2);
var count=0;
group.mouseover(function()
{
console.log('IN',++count);
});
group.mouseout(function()
{
console.log('OUT',++count);
});
and the CSS code:
#target{width:200px;}
Run the above code and see the results here: http://jsbin.com/ivules/7.
Console shows IN and OUT logs.
Just move the mouse between the two circles' bounds.
For your mouseout() function, please try:
group.mouseout(function()
{
this.mouseout(function(){
console.log('OUT',++count);
});
});
When you hover over the outer circle, you will get "IN". When you hover over the inner circle, you will get "IN" again. When you leave the circle entirely, you will finally get "OUT".
If that's too many "IN"'s, try creating an invisible circle, place it over top the current 2 circles, and only add the mouseover event to that circle. For instance, try:
c3=paper.circle(W/2,H/2,70).attr({fill:'orange','stroke-width':4,stroke:'red',opacity:0});
c3.mouseover(function()
{
console.log('IN',++count);
});
c3.mouseout(function()
{
console.log('OUT',++count);
});

Resources