Normalizando a velocidade da roda do mouse nos navegadores

147

Para uma pergunta diferente , compus esta resposta , incluindo este código de exemplo .

Nesse código, uso a roda do mouse para aumentar / diminuir o zoom de uma tela HTML5. Encontrei algum código que normaliza as diferenças de velocidade entre o Chrome e o Firefox. No entanto, o manuseio de zoom no Safari é muito, muito mais rápido do que em qualquer um deles.

Aqui está o código que tenho atualmente:

var handleScroll = function(e){
  var delta = e.wheelDelta ? e.wheelDelta/40 : e.detail ? -e.detail/3 : 0;
  if (delta) ...
  return e.preventDefault() && false;
};
canvas.addEventListener('DOMMouseScroll',handleScroll,false); // For Firefox
canvas.addEventListener('mousewheel',handleScroll,false);     // Everyone else

Qual código posso usar para obter o mesmo valor 'delta' para a mesma quantidade de roda do mouse rolando no Chrome v10 / 11, Firefox v4, Safari v5, Opera v11 e IE9?

Esta pergunta está relacionada, mas não tem uma boa resposta.

Edit : Investigações adicionais mostram que um evento de rolagem 'up' é:

                  | evt.wheelDelta | evt.detail
------------------ + ---------------- + ------------
  Safari v5 / Win7 | 120 0 0
  Safari v5 / OS X | 120 0 0
  Safari v7 / OS X | 12 0 0
 Chrome v11 / Win7 | 120 0 0
 Chrome v37 / Win7 | 120 0 0
 Chrome v11 / OS X | 3 (!) 0 (possivelmente errado)
 Chrome v37 / OS X | 120 0 0
        IE9 / Win7 120 Indefinido
  Opera v11 / OS X | 40 -1
  Opera v24 / OS X | 120 0 0
  Opera v11 / Win7 | 120 -3
 Firefox v4 / Win7 | indefinido | -3
 Firefox v4 / OS X | indefinido | -1
Firefox v30 / OS X | indefinido | -1

Além disso, o uso do trackpad do MacBook no OS X fornece resultados diferentes, mesmo quando se move devagar:

  • No Safari e Chrome, wheelDeltaé um valor de 3 em vez de 120 para a roda do mouse.
  • No Firefox , detailgeralmente é 2, às vezes 1, mas ao rolar muito lentamente, NÃO HANDLER DE EVENTOS FOI DE TODOS .

Então a questão é:

Qual é a melhor maneira de diferenciar esse comportamento (idealmente sem nenhum agente de usuário ou sistema operacional farejando)?

Phrogz
fonte
Desculpe, eu excluí minha pergunta. Estou escrevendo uma resposta agora. Antes de avançar, você está falando sobre a rolagem no Safari no Mac OS X? Quando você rola um pouco, ele rola um pouco, mas se você mantém uma taxa constante, fica progressivamente mais rápido?
Blender
@Blender Estou testando no OS X no momento e, sim, o Safari é o outlier, com zoom 20x mais rápido que o Chrome. Infelizmente, não tenho um mouse físico conectado, portanto, meus testes são restritos a furtos com dois dedos de distâncias e velocidades equivalentes.
Phrc #
Atualizei a pergunta com detalhes sobre o comportamento dos 5 principais navegadores no OS X e Win7. É um campo minado, com o Chrome no OS X aparentando ser o problema problemático.
Phrogz
@Phrogz Não deveria ser e.wheelDelta/120?
Šime Vidas
@ ŠimeVidas Sim, o código que copiei e estava usando estava claramente errado. Você pode ver um código melhor na minha resposta abaixo .
Phrogz 04/04

Respostas:

57

Editar setembro de 2014

Dado que:

  • Versões diferentes do mesmo navegador no OS X produziram valores diferentes no passado e podem fazê-lo no futuro, e isso
  • O uso do trackpad no OS X produz efeitos muito semelhantes ao uso da roda do mouse, mas fornece valores de eventos muito diferentes e, no entanto, a diferença do dispositivo não pode ser detectada pelo JS

… Só posso recomendar o uso deste código simples de contagem baseada em sinais:

var handleScroll = function(evt){
  if (!evt) evt = event;
  var direction = (evt.detail<0 || evt.wheelDelta>0) ? 1 : -1;
  // Use the value as you will
};
someEl.addEventListener('DOMMouseScroll',handleScroll,false); // for Firefox
someEl.addEventListener('mousewheel',    handleScroll,false); // for everyone else

A tentativa original de estar correta segue.

Aqui está minha primeira tentativa de um script para normalizar os valores. Ele tem duas falhas no OS X: o Firefox no OS X produzirá valores 1/3 do que deveriam ser e o Chrome no OS X produzirá valores 1/40 do que deveriam ser.

// Returns +1 for a single wheel roll 'up', -1 for a single roll 'down'
var wheelDistance = function(evt){
  if (!evt) evt = event;
  var w=evt.wheelDelta, d=evt.detail;
  if (d){
    if (w) return w/d/40*d>0?1:-1; // Opera
    else return -d/3;              // Firefox;         TODO: do not /3 for OS X
  } else return w/120;             // IE/Safari/Chrome TODO: /3 for Chrome OS X
};

Você pode testar esse código em seu próprio navegador aqui: http://phrogz.net/JS/wheeldelta.html

Sugestões para detectar e melhorar o comportamento no Firefox e Chrome no OS X são bem-vindas.

Editar : Uma sugestão do @Tom é simplesmente contar cada chamada de evento como um único movimento, usando o sinal da distância para ajustá-la. Isso não fornecerá ótimos resultados com rolagem suave / acelerada no OS X, nem manipulará perfeitamente os casos quando a roda do mouse for movida muito rapidamente (por exemplo, wheelDelta240), mas isso ocorre com pouca frequência. Agora, este código é a técnica recomendada mostrada na parte superior desta resposta, pelos motivos aqui descritos.

Phrogz
fonte
@ ŠimeVidas Graças, que é basicamente o que eu tenho, a não ser que eu também explicar a 1/3 diferença na Opera OS X.
Phrogz
@Phrogz, você tem uma versão atualizada em setembro de 2014 com todo o OS X / 3 adicionado? Isso seria um ótimo complemento para a comunidade!
Basj
@Phrogz, isso seria ótimo. Eu não tenho um Mac aqui para test ... (Eu ficaria feliz em dar uma recompensa para isso, mesmo que eu não tenho muita reputação me;))
Basj
1
No Windows Firefox 35.0.1, wheelDelta é indefinido e os detalhes são sempre 0, o que faz com que o código fornecido falhe.
Max strater
1
@MaxStrater Diante do mesmo problema, eu adicionei "deltaY" para superar isso em uma direção como essa, mas (((evt.deltaY <0 || evt.wheelDelta>0) || evt.deltaY < 0) ? 1 : -1)não sei o que o controle de qualidade descobre com isso.
Brock
28

Aqui está minha tentativa maluca de produzir um delta coerente e normalizado entre navegadores (-1 <= delta <= 1):

var o = e.originalEvent,
    d = o.detail, w = o.wheelDelta,
    n = 225, n1 = n-1;

// Normalize delta
d = d ? w && (f = w/d) ? d/f : -d/1.35 : w/120;
// Quadratic scale if |d| > 1
d = d < 1 ? d < -1 ? (-Math.pow(d, 2) - n1) / n : d : (Math.pow(d, 2) + n1) / n;
// Delta *should* not be greater than 2...
e.delta = Math.min(Math.max(d / 2, -1), 1);

Isso é totalmente empírico, mas funciona muito bem no Safari 6, FF 16, Opera 12 (OS X) e IE 7 no XP

smrtl
fonte
3
Se eu pudesse votar mais 10 vezes, eu poderia. Muito obrigado!
justnorris
Você pode ter o código funcional completo em uma demonstração (por exemplo, jsFiddle)?
Adardesign
Existe um motivo para armazenar em cache o eventobjeto-in o?
yckart 27/07
Não, não há. A ovariável está lá para mostrar que queremos o evento original e não um evento agrupado, como jQuery ou outras bibliotecas, pode passar para os manipuladores de eventos.
21913 smrtl
@smrtl você poderia explicar n e n1? Para que são essas variáveis?
Om3ga
28

Nossos amigos do Facebook reuniram uma ótima solução para esse problema.

Eu testei em uma tabela de dados que estou construindo usando o React e ela rola como manteiga!

Esta solução funciona em uma variedade de navegadores, no Windows / Mac, e ambos usando o trackpad / mouse.

// Reasonable defaults
var PIXEL_STEP  = 10;
var LINE_HEIGHT = 40;
var PAGE_HEIGHT = 800;

function normalizeWheel(/*object*/ event) /*object*/ {
  var sX = 0, sY = 0,       // spinX, spinY
      pX = 0, pY = 0;       // pixelX, pixelY

  // Legacy
  if ('detail'      in event) { sY = event.detail; }
  if ('wheelDelta'  in event) { sY = -event.wheelDelta / 120; }
  if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
  if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }

  // side scrolling on FF with DOMMouseScroll
  if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
    sX = sY;
    sY = 0;
  }

  pX = sX * PIXEL_STEP;
  pY = sY * PIXEL_STEP;

  if ('deltaY' in event) { pY = event.deltaY; }
  if ('deltaX' in event) { pX = event.deltaX; }

  if ((pX || pY) && event.deltaMode) {
    if (event.deltaMode == 1) {          // delta in LINE units
      pX *= LINE_HEIGHT;
      pY *= LINE_HEIGHT;
    } else {                             // delta in PAGE units
      pX *= PAGE_HEIGHT;
      pY *= PAGE_HEIGHT;
    }
  }

  // Fall-back if spin cannot be determined
  if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
  if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }

  return { spinX  : sX,
           spinY  : sY,
           pixelX : pX,
           pixelY : pY };
}

O código fonte pode ser encontrado aqui: https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js

George
fonte
3
Um link mais direto que não foi incluído no código original para normalizeWHeel.js github.com/facebook/fixed-data-table/blob/master/src/…
Robin Luiten
Obrigado @RobinLuiten, atualizando a postagem original.
George
Esse material é brilhante. Acabei de usá-lo e funciona como um encanto! Bom trabalho Facebook :)
perry
Você poderia dar um exemplo de como usá-lo? Eu tentei e funciona em FF, mas não no Chrome ou IE (11) ..? Obrigado
Andrew
2
Para quem usa o npm, existe um pacote pronto para usar, apenas desse código, extraído da Tabela de dados fixos do Facebook. Veja aqui para obter mais detalhes npmjs.com/package/normalize-wheel
Simon Watson
11

Criei uma tabela com valores diferentes retornados por diferentes eventos / navegadores, levando em consideração o DOM3 wheel evento que alguns navegadores já suportam (tabela abaixo).

Com base nisso, fiz esta função para normalizar a velocidade:

http://jsfiddle.net/mfe8J/1/

function normalizeWheelSpeed(event) {
    var normalized;
    if (event.wheelDelta) {
        normalized = (event.wheelDelta % 120 - 0) == -0 ? event.wheelDelta / 120 : event.wheelDelta / 12;
    } else {
        var rawAmmount = event.deltaY ? event.deltaY : event.detail;
        normalized = -(rawAmmount % 3 ? rawAmmount * 10 : rawAmmount / 3);
    }
    return normalized;
}

Tabela para mousewheel, wheele DOMMouseScrolleventos:

| mousewheel        | Chrome (win) | Chrome (mac) | Firefox (win) | Firefox (mac) | Safari 7 (mac) | Opera 22 (mac) | Opera 22 (win) | IE11      | IE 9 & 10   | IE 7 & 8  |
|-------------------|--------------|--------------|---------------|---------------|----------------|----------------|----------------|-----------|-------------|-----------|
| event.detail      | 0            | 0            | -             | -             | 0              | 0              | 0              | 0         | 0           | undefined |
| event.wheelDelta  | 120          | 120          | -             | -             | 12             | 120            | 120            | 120       | 120         | 120       |
| event.wheelDeltaY | 120          | 120          | -             | -             | 12             | 120            | 120            | undefined | undefined   | undefined |
| event.wheelDeltaX | 0            | 0            | -             | -             | 0              | 0              | 0              | undefined | undefined   | undefined |
| event.delta       | undefined    | undefined    | -             | -             | undefined      | undefined      | undefined      | undefined | undefined   | undefined |
| event.deltaY      | -100         | -4           | -             | -             | undefined      | -4             | -100           | undefined | undefined   | undefined |
| event.deltaX      | 0            | 0            | -             | -             | undefined      | 0              | 0              | undefined | undefined   | undefined |
|                   |              |              |               |               |                |                |                |           |             |           |
| wheel             | Chrome (win) | Chrome (mac) | Firefox (win) | Firefox (mac) | Safari 7 (mac) | Opera 22 (mac) | Opera 22 (win) | IE11      | IE 10 & 9   | IE 7 & 8  |
| event.detail      | 0            | 0            | 0             | 0             | -              | 0              | 0              | 0         | 0           | -         |
| event.wheelDelta  | 120          | 120          | undefined     | undefined     | -              | 120            | 120            | undefined | undefined   | -         |
| event.wheelDeltaY | 120          | 120          | undefined     | undefined     | -              | 120            | 120            | undefined | undefined   | -         |
| event.wheelDeltaX | 0            | 0            | undefined     | undefined     | -              | 0              | 0              | undefined | undefined   | -         |
| event.delta       | undefined    | undefined    | undefined     | undefined     | -              | undefined      | undefined      | undefined | undefined   | -         |
| event.deltaY      | -100         | -4           | -3            | -0,1          | -              | -4             | -100           | -99,56    | -68,4 | -53 | -         |
| event.deltaX      | 0            | 0            | 0             | 0             | -              | 0              | 0              | 0         | 0           | -         |
|                   |              |              |               |               |                |                |                |           |             |           |
|                   |              |              |               |               |                |                |                |           |             |           |
| DOMMouseScroll    |              |              | Firefox (win) | Firefox (mac) |                |                |                |           |             |           |
| event.detail      |              |              | -3            | -1            |                |                |                |           |             |           |
| event.wheelDelta  |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.wheelDeltaY |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.wheelDeltaX |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.delta       |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.deltaY      |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.deltaX      |              |              | undefined     | undefined     |                |                |                |           |             |           |
Sergio
fonte
2
Resultados em diferentes velocidades de rolagem no atual Safari e Firefox no macOS.
Lenar Hoyt 17/07/07
6

Outra solução mais ou menos independente ...

Porém, isso não leva tempo entre os eventos. Alguns navegadores parecem sempre acionar eventos com o mesmo delta e apenas acioná-los mais rapidamente ao rolar rapidamente. Outros variam os deltas. Pode-se imaginar um normalizador adaptável que leva tempo em consideração, mas que seria um pouco complicado e complicado de usar.

Trabalho disponível aqui: jsbin / iqafek / 2

var normalizeWheelDelta = function() {
  // Keep a distribution of observed values, and scale by the
  // 33rd percentile.
  var distribution = [], done = null, scale = 30;
  return function(n) {
    // Zeroes don't count.
    if (n == 0) return n;
    // After 500 samples, we stop sampling and keep current factor.
    if (done != null) return n * done;
    var abs = Math.abs(n);
    // Insert value (sorted in ascending order).
    outer: do { // Just used for break goto
      for (var i = 0; i < distribution.length; ++i) {
        if (abs <= distribution[i]) {
          distribution.splice(i, 0, abs);
          break outer;
        }
      }
      distribution.push(abs);
    } while (false);
    // Factor is scale divided by 33rd percentile.
    var factor = scale / distribution[Math.floor(distribution.length / 3)];
    if (distribution.length == 500) done = factor;
    return n * factor;
  };
}();

// Usual boilerplate scroll-wheel incompatibility plaster.

var div = document.getElementById("thing");
div.addEventListener("DOMMouseScroll", grabScroll, false);
div.addEventListener("mousewheel", grabScroll, false);

function grabScroll(e) {
  var dx = -(e.wheelDeltaX || 0), dy = -(e.wheelDeltaY || e.wheelDelta || 0);
  if (e.detail != null) {
    if (e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
    else if (e.axis == e.VERTICAL_AXIS) dy = e.detail;
  }
  if (dx) {
    var ndx = Math.round(normalizeWheelDelta(dx));
    if (!ndx) ndx = dx > 0 ? 1 : -1;
    div.scrollLeft += ndx;
  }
  if (dy) {
    var ndy = Math.round(normalizeWheelDelta(dy));
    if (!ndy) ndy = dy > 0 ? 1 : -1;
    div.scrollTop += ndy;
  }
  if (dx || dy) { e.preventDefault(); e.stopPropagation(); }
}
Marijn
fonte
1
Esta solução não funciona de todo com o Chrome no Mac com o Trackpad.
justnorris
@Norris, acredito que sim agora. Só encontrei esta questão eo exemplo aqui funciona no meu macbook com cromo
Harry Moreno
4

Solução simples e funcional:

private normalizeDelta(wheelEvent: WheelEvent):number {
    var delta = 0;
    var wheelDelta = wheelEvent.wheelDelta;
    var deltaY = wheelEvent.deltaY;
    // CHROME WIN/MAC | SAFARI 7 MAC | OPERA WIN/MAC | EDGE
    if (wheelDelta) {
        delta = -wheelDelta / 120; 
    }
    // FIREFOX WIN / MAC | IE
    if(deltaY) {
        deltaY > 0 ? delta = 1 : delta = -1;
    }
    return delta;
}
Marek
fonte
3

Para suporte ao zoom em dispositivos touch, registre-se nos eventos gesturestart, gesturechange e gestend e use a propriedade event.scale. Você pode ver o código de exemplo para isso.

Para o Firefox 17, o onwheelevento está planejado para ser suportado pelas versões para desktop e móvel (conforme os documentos MDN onwheel ). Também para o Firefox, talvez o MozMousePixelScrollevento específico do Gecko seja útil (embora presumivelmente isso agora esteja obsoleto, pois o evento DOMMouseWheel agora está obsoleto no Firefox).

Para Windows, o próprio driver parece gerar os eventos WM_MOUSEWHEEL, WM_MOUSEHWHEEL (e talvez o evento WM_GESTURE para a panorâmica do touchpad?). Isso explicaria por que o Windows ou o navegador não parecem normalizar os próprios valores do evento do mousewheel (e pode significar que você não pode escrever um código confiável para normalizar os valores).

Para suporte a eventos onwheel( não onmousewheel) no Internet Explorer para IE9 e IE10, você também pode usar o evento padrão W3C onwheel . No entanto, um ponto pode ser um valor diferente de 120 (por exemplo, um único ponto se torna 111 (em vez de -120) no mouse usando esta página de teste ). Escrevi outro artigo com outros detalhes sobre os eventos da roda que podem ser relevantes.

Basicamente, em meus próprios testes para eventos de roda (estou tentando normalizar os valores para rolagem), descobri que recebo valores variados para SO, fornecedor de navegador, versão de navegador, tipo de evento e dispositivo (mouse Microsoft tiltwheel, gestos do touchpad do laptop , touchpad para laptop com scrollzone, mouse mágico da Apple, scrollball poderoso da Apple, touchpad para Mac, etc.).

E precisa ignorar vários efeitos colaterais da configuração do navegador (por exemplo, Firefox mousewheel.enable_pixel_scrolling, chrome --soll-pixels = 150), configurações do driver (por exemplo, Synaptics touchpad) e configuração do sistema operacional (configurações do mouse do Windows, preferências do mouse OSX, Configurações do botão X.org).

robocat
fonte
2

Este é um problema com o qual estou lutando há algumas horas hoje e não pela primeira vez :(

Eu tenho tentado resumir valores com um "toque" e ver como diferentes navegadores relatam valores, e eles variam muito, com o Safari relatando números de ordem maior em quase todas as plataformas, o Chrome relatando muito mais (três vezes mais ) do que o firefox, o firefox sendo equilibrado a longo prazo, mas bem diferente entre plataformas em pequenos movimentos (no gnomo Ubuntu, quase apenas +3 ou -3, parece que resume eventos menores e envia um grande "+3")

As soluções atuais encontradas agora são três:

  1. O já mencionado "use apenas o sinal" que mata qualquer tipo de aceleração
  2. Cheire o navegador até a versão e plataforma secundárias e ajuste-o corretamente
  3. A Qooxdoo implementou recentemente um algoritmo de auto-adaptação, que basicamente tenta dimensionar o delta com base no valor mínimo e máximo recebido até o momento.

A ideia no Qooxdoo é boa e funciona, e é a única solução que atualmente achei completamente consistente entre navegadores.

Infelizmente, tende a renormalizar também a aceleração. Se você tentar (nas demos) e rolar para cima e para baixo na velocidade máxima por um tempo, perceberá que a rolagem extremamente rápida ou extremamente lenta produz basicamente basicamente a mesma quantidade de movimento. Pelo contrário, se você recarregar a página e deslizar muito lentamente, notará que ela rolará muito rápido ".

Isso é frustrante para um usuário de Mac (como eu) costumava dar vigorosos movimentos de rolagem no touchpad e esperando chegar ao topo ou ao fundo da coisa rolada.

Ainda mais, como reduz a velocidade do mouse com base no valor máximo obtido, quanto mais o usuário tenta acelerá-lo, mais lento, enquanto um usuário de "rolagem lenta" experimenta velocidades bastante rápidas.

Isso torna essa solução (caso contrário brilhante) uma implementação ligeiramente melhor da solução 1.

Portei a solução para o plugin jquery mousewheel: http://jsfiddle.net/SimoneGianni/pXzVv/

Se você jogar por um tempo, verá que começará a obter resultados bastante homogêneos, mas também perceberá que ele tende a + 1 / -1 com valores muito rápidos.

Agora estou trabalhando para aprimorá-lo para detectar picos melhor, para que eles não enviem tudo "fora de escala". Também seria bom obter também um valor flutuante entre 0 e 1 como valor delta, para que haja uma saída coerente.

Simone Gianni
fonte
1

Definitivamente, não há uma maneira simples de normalizar todos os usuários em todos os SOs em todos os navegadores.

Fica pior do que as variações listadas - na instalação do WindowsXP + Firefox3.6, a roda do mouse faz 6 por rolagem de um ponto - provavelmente porque em algum lugar que esqueci eu acelerei a roda do mouse, no sistema operacional ou em algum lugar: config

No entanto, estou trabalhando em um problema semelhante (com um aplicativo semelhante, mas não em tela) e isso ocorre comigo apenas usando o sinal delta de +1 / -1 e medindo ao longo do tempo na última vez em que foi acionado, tem uma taxa de aceleração, ou seja. se alguém rola uma vez vs várias vezes em alguns momentos (o que eu apostaria é como o Google Maps faz isso).

O conceito parece funcionar bem nos meus testes, basta adicionar menos de 100ms à aceleração.

ck_
fonte
-2
var onMouseWheel = function(e) {
    e = e.originalEvent;
    var delta = e.wheelDelta>0||e.detail<0?1:-1;
    alert(delta);
}
$("body").bind("mousewheel DOMMouseScroll", onMouseWheel);
Matthieu Chavigny
fonte