I'm working on a D3 application and have bumped into a strange zooming issue that occurs in Chrome but not Firefox (these are the only browsers I've tested my code in). I've boiled the problem down to the code snippet below.
Basically, in certain conditions, the zoom handler I have defined does not get called when I scroll over my canvas element (using either my mouse's scroll button or two fingers on my laptop's trackpad). However, clicking and dragging on the canvas to try to pan it does successfully invoke the zoom handler.
Details of the issue:
Zooming works as expected as long as the "click" event handler does not get called. Once it's called, scroll zooming will not work. And it has something to do with invoking ctx.getImageData in the click event handler.
If I omit the .call at the end of the canvas selection that invokes clearRect, the zooming issue occurs even if you did not trigger a "click" event
The issue occurs in other versions of D3 besides v5. I've tried v6 and v7.
The Chrome version I'm using is 92.0.4515.159
There are a couple ways I know of to resolve the issue:
The issue is resolved if the canvas element's opacity is not set to 0.
It can also be resolved by calling window.addEventListener('wheel', () => {})
From what I recall, another solution is defining the zoom behavior on a parent element of the canvas instead.
The code below creates a canvas element in the top left corner (it's not visible since its opacity is 0). Zooming on the element will print a "zooming" message to the console. Clicking on the canvas will print "click". You'll find that with the code as is, if you click on the element and then try to scroll zoom, "zooming" will not get printed. You can also run the code in JSFiddle: https://jsfiddle.net/yr3jdvhw/37/
I'd like to know what's causing this issue. As I mentioned, I could just set the canvas's opacity to a non-zero value to avoid the problem. But I'd really like to know the root cause.
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<canvas class="my-canvas"></canvas>
<script>
const myCanvas = d3.select(".my-canvas")
.style('opacity', 0)
.on('click', function () {
console.log("click")
let ctx = this.getContext('2d')
const pixel = ctx.getImageData(0, 0, 1, 1)
})
.call(d3.zoom().on('zoom', () => {
console.log("zooming")
}))
.call(function (s) {
let ctx = s.node().getContext('2d')
ctx.clearRect(0, 0, s.node().width, s.node().height)
})
</script>
</body>
</html>
This is a Chrome bug, I just opened a new issue about it, let's hope this will get fixed soon.
From my investigations, the root issues are that they do handle wheel events at the compositor level and that when your canvas gets deaccelerated, it doesn't take the good path in the compositor anymore and thus isn't seen as a "wheel event handler region" anymore.
Calling getImageData() currently deaccelerates your canvas, it goes from the GPU to the CPU and stays there, which is why calling this method causes the issue. Similarly, until you perform any drawing operation, the canvas isn't moved to the GPU yet, and thus here too the bug reproduces.
Note that hopefully in a few versions { willReadFrequently } will be available without a flag, and that at this moment most canvases will always be accelerated.
Related
When using HTML 5 drag and drop, i have noticed that on firefox, the ghost image tends to fly in from far away to the right. I have not been able to reproduce using a small scale example, so i assume it has something to do with the CSS involved. Unfortunately, this is part of a huge application, so teasing out a sandbox wasnt possible, but i included a gif of the behavior. Drag and drop seems to function correctly on chrome and other browsers. This is the code that occurs on dragstart (sectionalGrid is the div that comprises the 5x5 grid)
document.getElementById('sectionalGrid').addEventListener('dragstart', e=> {
const gridItemRoot = findAncestor(e.target, 'mxt-grid-item');
e.dataTransfer.setData("grid_x", gridItemRoot.dataset.x);
e.dataTransfer.setData("grid_y", gridItemRoot.dataset.y);
this._dragStartMode = 'gridItem';
document.getElementById('sectionalRemovePiece').classList.add('sectionalHighlightRemove');
});
I'm building an app in HTML5, using CreateJS (EaselJS), Box2D and written in CoffeeScript using Browserify (built with Gulp).
I've just spent the best part of a half a day beating my head against the keyboard trying to work out why I couldn't get my EaselJS events working. Window/document events were fine. Any and all EaselJS event just did nothing. It turns out the debug canvas was trapping them, overlaying invisibly.
I accidentally discovered this eventually by toggling the debug canvas on and off which allowed the events to get though and they started responding.
So now, my question is: what's the best way to handle events so that they will work
a) without me first toggling the debug canvas, and
b) when debug is active, how can I pass events through to the main canvas below so that it can handle them?
Is that possible? It would be ideal that even with debug draw on, mouseevents worked as normal.
HTML is this:
<body>
<canvas id="arcade"></canvas>
<canvas id="debug"></canvas>
<button id="toggle-debug">Toggle Debug</button>
<script src="compiled.js"></script>
</body>
My app class sets up events calling bindEvents from its constructor thus:
bindEvents: =>
# Pause when window is blurred
window.addEventListener 'blur', #setPaused
# Play when window is focused
window.addEventListener 'focus', #setPlay
# Toggle showing/hiding of the Debug <canvas>
document.getElementById('toggle-debug').onclick = #toggleDebug
toggleDebug: =>
#showDebug = !#showDebug
# Shows/hides Debug canvas
Universe.debug.canvas.style.display = (if #showDebug then 'block' else 'none')
setPlay: =>
#play = true
createjs.Ticker.setPaused false
setPaused: =>
#play = false
createjs.Ticker.setPaused true
My objects (which contain box2d body in #body and easelJS display objects in #view) set up their events in a similar method, e.g.
bindEvents: ->
#view.on "click", () => alert("ball #{#options.id} clicked")
Unfortunately the DOM does not provide a good way to ignore mouse events on an object, or pass them through. You can however use the Stage.nextStage property.
Create an EaselJS stage, and pass in your debug canvas
Set the nextStage property on the new Stage to the stage that is below that one.
Here is a quick code sample:
var stage2 = new createjs.Stage("DebugCanvasId");
stage2.nextStage = stage1;
EaselJS 0.7.0 added the nextStage property, but the very latest NEXT version in GitHub vastly improved support for this. There is a demo in GitHub where you can see this working (TwoStages.html). Here is the online sample: http://createjs.com/Demos/EaselJS/TwoStages.html
Cheers.
I've solved part a) by changing the html to
<canvas id="debug" style="display: none;"></canvas>
I also tried doing this programmatically in the game (toggling the debug twice) but for some reason it didn't work. I don't know why.
I'd still like to get an answer to B though. How to pass mouse events though the debug canvas to the one below.
This one is getting tricky for me.I have the google map in my page which is working properly ,above it lies a canvas. I need to make the google map clickable .i.e when i click on the canvas ,the map should behave normally .I have added pointer-events:none;attribute.It works properly in Firefox ,chrome and IE11.
However my requirement is I need to make it clickable in IE9 on wards,which am unable to replicate. How to do that?
If any one can replicate the behavior in a fiddle ,that will be really helpful to me.
There's an old jQuery hack that simulates pointer-events:
Listen for a click event on the canvas and in the click handler:
Hide the canvas: $(this).hide();
Ask the document to get the element at the clicked xy: var $map=$(document.elementFromPoint(event.clientX,event.clientY); (Adjust clientX/Y by any offset to the map/canvas elements) If the map is the only element under your canvas, you could define $map at the start of your app and avoid this step.
trigger the same event on the google map: $map.trigger(event);
redisplay the canvas: $(this).show();
I've earlier successfully used the JavaScriptAudioNode in the Web Audio API to synthesize and mix audio both in Chrome and Safari 6.0. However, the latest version of Safari no longer appears to work, because it does not call onaudioprocess to fill the source buffers.
This is a simplified example which plays only silence and appends text to the document body on each call to onaudioprocess:
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$("a").click(function() {
var context = new webkitAudioContext();
var mixerNode=context.createJavaScriptNode(2048, 0, 2);
mixerNode.onaudioprocess=function(ape) {
var buffer=ape.outputBuffer;
for(var s=0;s<buffer.length;s++)
{
buffer.getChannelData(0)[s]=0;
buffer.getChannelData(1)[s]=0;
}
$("body").append("buffering<br/>");
};
$("body").html("");
mixerNode.connect(context.destination);
return false;
});
});
</script>
</head>
<body>
start
</body>
</html>
The above example works in Chrome as expected, but not in desktop Safari. The iOS version of Safari does not work either, but it never did work for me in the first place.
Calling context.createJavaScriptNode does return a proper object of type JavaScriptAudioNode and connecting it to the destination node does not throw any exceptions. context.activeSourceCount remains at zero, but this is also the case in Chrome as it apparently only counts active nodes of type AudioBufferSourceNode. context.currentTime also increments as expected.
Am I doing something wrong here or is this an actual bug or a missing feature in Safari? The Apple documentation has no mention of JavaScriptAudioNode (nor the new name, ScriptProcessorNode) but it did work before on the first release of Safari 6. The iOS Safari requirement for user input doesn't seem to help, as the example above should take care of that.
The simple example can be found here and a more complex one is my Protracker module player which exhibits the same behaviour.
There are a couple bugs in Safari's implementation of the Web Audio API that you'll need to look out for. The first is in the createJavaScriptNode constructor... it seems to have problems with the "input channels" param being set to 0. Try changing it to this:
createJavaScriptNode(2048, 1, 2)
The second issue has to do with garbage collection (I think); once your mixerNode variable is out of scope, Safari seems to stop firing the onaudioprocess callback. One solution is to introduce mixerNode at the top-level scope (i.e. declaring var mixerNode; at the top of your script) and then store your JavaScriptNode in that top-level variable. If you plan on dynamically creating multiple mixerNodes, you can achieve the same effect by storing references to them in a top-level array variable.
If you make these two changes (input channel param set to 1, maintaining a reference to the mixerNode) then your script should work in Safari as expected.
I have created a jQuery window scroll-bar detector but when I re-size the browser window, in-turn changing the scroll-bar status, either from visible to not visible or vice-versa, the status notifier remains static. How can I keep the status notifier continuously updated on every re-size in a simple way without AJAX calls, via strictly javascript?
For further context, I am basically trying to come up with a solution for semi-infinite-scrolling if the content dosn't exceed the window size. If one of you guys figures this out it will be very benifical for a lot of UI developers trying to incorporate the highly demanded and popular load-on-scroll effect. Thanks in advance. Here is my demo.
$(window).resize(function () {
///????
});
You can use so-called functions to achieve this. In Javascript, functions are actually objects and can be simply stored in variables and passed around as parameters.
Take this example:
//first define your function - maybe give it a better name than I did :)
var detectScrollbar = function () {
if($(window).height() >= $(document).height()){
$('#statusNotifier1').fadeIn("slow");
$('#statusNotifier2').hide();
}
else
{
$('#statusNotifier1').hide();
$('#statusNotifier2').fadeIn("slow");
}
};
//call it initially
detectScrollbar();
//pass it to .resize() so it will be called when the event fires
$(window).resize(detectScrollbar);
You could put this whole thing into your $(document).ready();.
Have fun with the jsFiddle Demo. I hope all those sad UI developers will be dancing around with smiles on their face, celebrating :).