20

Такой вопрос, как сделать Radial blur с помощью SVG? Порыв в гугле я понял что либо он в SVG не так называется, либо способа нет...

Radial blur

  • Смотрите в сторону фильтров feDisplacementMap и feTurbulence. Вот неплохой пример https://ru.stackoverflow.com/a/825600/275019 – zhurof Feb 25 '19 at 10:05
  • @ПавелВаршавский я вот например готовлю ответ, это тоже занимает время – Stranger in the Q Mar 02 '19 at 11:18

3 Answers3

16

Это конечно не CSS, как и в других моих постах последнее время, будем окучивать WebGL / glsl, (мало внимания ему уделяется, это заметно по просмотрам)

Я написал маленькую функцию, чтобы вынести за скобки весь WebGL код, оставив один лишь фрагментный шейдер на входе:


document.body.append(webglFilter("https://i.imgur.com/cdqfoqZ.png",  `

    const int samples = 22;               // кол-во сэмплов
    const float power = 0.001;            // сила эффекта
    const vec2 center = vec2( 0.5, 0.5 ); // центр эффекта

    // матрица поворота
    mat2 rotate2d (float angle) {
        vec2 sc = vec2( sin(angle), cos(angle) );
        return mat2( sc.y, -sc.x, sc.xy );
    }

    vec4 frag (vec2 uv) {

        vec4 color = vec4(0.);                          // аккумулятор
        for (int i = 0; i < samples; i++) {

            float dir = sin(length(uv - center)*200.);  // направление повотора
            dir = smoothstep(-.5, .5, dir) - .5;        // делаем из синусоиды сглаженную 
                                                        // ступенчатую функцию

            uv -= center;                               // сдвиг к центру поворота
            uv *= rotate2d( dir * power * float(i) );   // поворот
            uv += center;                               // обратный сдвиг
            color += sample(uv);                        // получить цвет
        }   

        return color / float(samples);                  // взять среднее
    }

`));

UPD1: попытался лучше повторить эффект на оригинальной картинке

UPD2: добавлена реакция на мышь

UPD3: radial blur теперь применяется плавно

UPD4: на мобилке теперь видно эффекты

UPD5: добавил еще сниппет


Версия со ступенчатой функцией поворота

tasty human burgers

let filter = webglFilter("https://i.imgur.com/cdqfoqZ.png", `

const int samples = 22; uniform float power; uniform vec2 mouse;

mat2 rotate2d (float angle) { vec2 sc = vec2( sin(angle), cos(angle) ); return mat2( sc.y, -sc.x, sc.xy ); }

vec4 frag (vec2 uv) {

float rotateDir = sin(length(uv - mouse)*1./(0.005 + power*5.));
rotateDir = smoothstep(-.3, .3, rotateDir)-.5;

vec2 shiftDir = (uv-mouse)*vec2(-1.0,-1.0);

vec4 color = vec4(0.);
for (int i = 0; i &lt; samples; i ++) {
  uv += float(i)/float(samples)*shiftDir*0.01;
  uv -= mouse;      
  uv *= rotate2d( rotateDir * power * float(i)); 
  uv += mouse;
  color += sample(uv) / float(samples);
} 
return color;

}

`);

let changeCenter = function(e) { e = e.touches ? e.touches[0] : e; let c = filter.canvas; let z = window.getComputedStyle(c).zoom; let d = document.documentElement; let x = (e.clientX + d.scrollLeft - c.offsetLeftz) / c.width / z; let y = (e.clientY + d.scrollTop - c.offsetTopz) / c.height / z filter.uniform('2f', 'mouse', x, y).apply(); }

var applyEffect = (function() {

let power = 0;
let targ = 0
let started = 0;

return function (pow) {
  targ = pow;
  started = new Date().getTime();
  requestAnimationFrame(animate);
}

function animate() {
  let dt = new Date().getTime() - started; 
  power += dt * 1e-6 * (targ === 0 ? -1 : 1);
  power = Math[targ === 0 ? 'max' : 'min'](power, targ);
  filter.uniform('1f', 'power', power).apply();
  Math.abs(power-targ) &gt; 1e-7 &amp;&amp; requestAnimationFrame(animate)
}

})();

filter.ready = function() {

let c = filter.canvas; document.body.append(c); let z = window.getComputedStyle(c).zoom; changeCenter({ clientX: c.width/2z, clientY: c.height/2z, }); applyEffect(0.001);

filter.apply();

window.addEventListener('mousemove', e => changeCenter(e)) window.addEventListener('touchmove', e => changeCenter(e)) window.addEventListener('mouseup', () => applyEffect(0)) window.addEventListener('touchend', () => applyEffect(0)) window.addEventListener('mousedown', () => applyEffect(0.001)) window.addEventListener('touchstart', () => applyEffect(0.001))

}

canvas {
  zoom: 33%;
}
<script>
function webglFilter(url, fragCode) {

  let canvas = document.createElement('canvas');
  let pid, gl = canvas.getContext('webgl') 
        || canvas.getContext('experimental-webgl');

  let loader = new Image();
  loader.crossOrigin = "anonymous";
  loader.src = url;

  loader.onload = function() { 

    canvas.width = loader.width;
    canvas.height = loader.height;

    pid = gl.createProgram();

    shader(`
      attribute vec2 coords;
        void main(void) {
        gl_Position = vec4(coords.xy, 0.0, 1.0);
      }
    `, gl.VERTEX_SHADER);

    shader(`
      precision highp float;
      uniform sampler2D texture;

      vec4 sample(vec2 uv) {
          return texture2D(texture, uv);
      }

      ${fragCode}

      void main(void) {
          gl_FragColor = frag(vec2( 
            gl_FragCoord.x / ${canvas.width}.,  
            1. - gl_FragCoord.y / ${canvas.height}. 
          ));
      }
    `, gl.FRAGMENT_SHADER);

    gl.linkProgram(pid);
    gl.useProgram(pid);

    let array = new Float32Array([-1,  3, -1, -1, 3, -1]);
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);

    let al = gl.getAttribLocation(pid, "coords");
    gl.vertexAttribPointer(al, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(al);

    let texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, loader);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

    var textureLocation = gl.getUniformLocation(pid, "texture");
    gl.uniform1i(textureLocation, 0);

    filter.ready && filter.ready();

    function shader(src, type) {
      let sid = gl.createShader(type);
      gl.shaderSource(sid, src);
      gl.compileShader(sid);
      var message = gl.getShaderInfoLog(sid);
      gl.attachShader(pid, sid);
      if (message.length > 0) {
        console.log(src.split('\n').map(function (str, i) {
          return ("" + (1 + i)).padStart(4, "0") + ": " + str
        }).join('\n'));
        throw message;
      }
    }
  }

  let filter = {

    canvas: canvas,

    ready: null,

    uniform: function(type, name, v1, v2, v3, v4) {
      if (!pid)
        throw new Error('image not loaded yet');
      var ul = gl.getUniformLocation(pid, name);
      gl['uniform' + type](ul, v1, v2, v3, v4);
      return filter;
    },

    apply: function() {
      if (!pid)
        throw new Error('image not loaded yet');
      gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      gl.clearColor(0, 0, 0, 0);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
      return filter;
    }
  }

  return filter;
}
</script>

Обычный radial blur:

введите сюда описание изображения

let filter = webglFilter("https://i.imgur.com/tbmyMTo.jpg", `

const int samples = 66; uniform float power; uniform vec2 mouse;

mat2 rotate2d (float angle) { vec2 sc = vec2( sin(angle), cos(angle) ); return mat2( sc.y, -sc.x, sc.xy ); }

vec4 frag (vec2 uv) {

float rotateDir = length(uv - mouse)*1./(0.005 + power*5.);
rotateDir = smoothstep(-.3, .3, rotateDir)-.5;

vec2 shiftDir = (uv-mouse)*vec2(-1.0,-1.0);

vec4 color = vec4(0.);
for (int i = 0; i &lt; samples; i ++) {
  uv += float(i)/float(samples)*shiftDir*0.01;
  uv -= mouse;      
  uv *= rotate2d( rotateDir * power * float(i)); 
  uv += mouse;
  color += sample(uv)/float(samples+i);
} 
return color*1.5;

}

`);

let changeCenter = function(e) { e = e.touches ? e.touches[0] : e; let c = filter.canvas; let z = window.getComputedStyle(c).zoom; let d = document.documentElement; let x = (e.clientX + d.scrollLeft - c.offsetLeftz) / c.width / z; let y = (e.clientY + d.scrollTop - c.offsetTopz) / c.height / z filter.uniform('2f', 'mouse', x, y).apply(); }

var applyEffect = (function() {

let power = 0;
let targ = 0
let started = 0;

return function (pow) {
  targ = pow;
  started = new Date().getTime();
  requestAnimationFrame(animate);
}

function animate() {
  let dt = new Date().getTime() - started; 
  power += dt * 1e-6 * (targ === 0 ? -1 : 1);
  power = Math[targ === 0 ? 'max' : 'min'](power, targ);
  filter.uniform('1f', 'power', power).apply();
  Math.abs(power-targ) &gt; 1e-7 &amp;&amp; requestAnimationFrame(animate)
}

})();

filter.ready = function() {

let c = filter.canvas; document.body.append(c); let z = window.getComputedStyle(c).zoom; changeCenter({ clientX: c.width/2z, clientY: c.height/2z, }); applyEffect(0.001);

filter.apply();

window.addEventListener('mousemove', e => changeCenter(e)) window.addEventListener('touchmove', e => changeCenter(e)) window.addEventListener('mouseup', () => applyEffect(0)) window.addEventListener('touchend', () => applyEffect(0)) window.addEventListener('mousedown', () => applyEffect(0.001)) window.addEventListener('touchstart', () => applyEffect(0.001))

}

canvas {
zoom:33%
}
<script>
function webglFilter(url, fragCode) {

  let canvas = document.createElement('canvas');
  let pid, gl = canvas.getContext('webgl') 
        || canvas.getContext('experimental-webgl');

  let loader = new Image();
  loader.crossOrigin = "anonymous";
  loader.src = url;

  loader.onload = function() { 

    canvas.width = loader.width;
    canvas.height = loader.height;

    pid = gl.createProgram();

    shader(`
      attribute vec2 coords;
        void main(void) {
        gl_Position = vec4(coords.xy, 0.0, 1.0);
      }
    `, gl.VERTEX_SHADER);

    shader(`
      precision highp float;
      uniform sampler2D texture;

      vec4 sample(vec2 uv) {
          return texture2D(texture, uv);
      }

      ${fragCode}

      void main(void) {
          gl_FragColor = frag(vec2( 
            gl_FragCoord.x / ${canvas.width}.,  
            1. - gl_FragCoord.y / ${canvas.height}. 
          ));
      }
    `, gl.FRAGMENT_SHADER);

    gl.linkProgram(pid);
    gl.useProgram(pid);

    let array = new Float32Array([-1,  3, -1, -1, 3, -1]);
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);

    let al = gl.getAttribLocation(pid, "coords");
    gl.vertexAttribPointer(al, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(al);

    let texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, loader);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

    var textureLocation = gl.getUniformLocation(pid, "texture");
    gl.uniform1i(textureLocation, 0);

    filter.ready && filter.ready();
    filter.apply();

    function shader(src, type) {
      let sid = gl.createShader(type);
      gl.shaderSource(sid, src);
      gl.compileShader(sid);
      var message = gl.getShaderInfoLog(sid);
      gl.attachShader(pid, sid);
      if (message.length > 0) {
        console.log(src.split('\n').map(function (str, i) {
          return ("" + (1 + i)).padStart(4, "0") + ": " + str
        }).join('\n'));
        throw message;
      }
    }
  }

  let filter = {

    canvas: canvas,

    ready: null,

    uniform: function(type, name, v1, v2, v3, v4) {
      if (!pid)
        throw new Error('program not ready');
      var ul = gl.getUniformLocation(pid, name);
      gl['uniform' + type](ul, v1, v2, v3, v4);
      return filter;
    },

    apply: function() {
      if (!pid)
        throw new Error('program not ready');
      gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      gl.clearColor(0, 0, 0, 0);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
      return filter;
    }
  }

  return filter;
}
</script>

Самая актуальная версия тут
14

Не претендую на участие, ибо подхожу по меткам лишь частично, да и реализация - "костыль костыльный". Но, тем не менее, пока не дали ответы с SVG, нативный вариант:

var nDivs = 90;
var oWrap = document.querySelector('div.radial_blur');
var oDiv = document.createElement('DIV');
var oTemp;

/* Раскомментировать, если нужен только эффект, без анимации while (nDivs--) { oTemp = oWrap.querySelector('.radial_blur :empty').appendChild(oDiv.cloneNode(true)); oTemp.style.transform = 'rotate(1deg)'; } /

/* Код ниже, только для наглядной демонстрации / function radialBlur() { oTemp = oWrap.querySelector('.radial_blur :empty').appendChild(oDiv.cloneNode(true)); oTemp.style.transform = 'rotate(1deg)'; nDivs--; if (!nDivs) { clearInterval(nIntervId); } } var nIntervId = window.setInterval(radialBlur, 80);

.radial_blur {
  margin: 20px auto;
  height: 400px;
  width: 435px;
  overflow: hidden;
  border: 3px solid #000;
  background: url('https://i.imgur.com/CQvNGGD_d.jpg?maxwidth=520&shape=thumb&fidelity=high') center/cover;
  /* Можно попробовать с другим изображением
  background: url('https://i.imgur.com/cdqfoqZ.png') center/cover; */
}

.radial_blur div {
  height: inherit;
  width: inherit;
  background: inherit;
  opacity: .95;
}
<div class="radial_blur">
  <div></div>
</div>
UModeL
  • 34,026
  • 6
  • 29
  • 71
  • смотрел с мобилы работает ... – Резидент Казахстана Mar 03 '19 at 03:35
  • если до субботы автор не выберет от какой то ответ ...то отмечу этот ответ решением – Резидент Казахстана Mar 05 '19 at 23:18
  • @ПавелВаршавский спасибо. Но, я прошу Вас выбрать более достойные примеры. Ребята продвигают технологии, когда я пользуюсь нативными "костылями" (за которые нормальные фронтэндщики могут и побить :-) . Будущее за технологиями, а я лишь показал, что "хоть и криво, но возможно". – UModeL Mar 06 '19 at 02:11
  • Как выбрать ) к примеру мне твой нравится, ну и так же два остальных тоже ...блин – Резидент Казахстана Mar 06 '19 at 12:27
  • @ПавелВаршавский я здесь сижу, чтобы подглядывать, резко выпрыгивать и показывать))) В общем, для удовольствия)) Самая большая награда - это, когда кусок твоего кода используют даже не допиливая (хотя, это и расслабляет вопрошающих). Пока у меня нет стоящих вопросов, а так бы надо замутить, да поделиться репой незаслуженной)) – UModeL Mar 06 '19 at 13:02