Такой вопрос, как сделать Radial blur с помощью SVG? Порыв в гугле я понял что либо он в SVG не так называется, либо способа нет...
Ответ Это конечно не 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: добавил еще сниппет
Версия со ступенчатой функцией поворота
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 < 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.offsetLeft*z) / c.width / z let y = (e.clientY + d.scrollTop - c.offsetTop*z) / 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) > 1e-7 && requestAnimationFrame(animate) } })() filter.ready = function() { let c = filter.canvas document.body.append(c) let z = window.getComputedStyle(c).zoom changeCenter({ clientX: c.width/2*z, clientY: c.height/2*z, }) 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% } 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(' ').map(function (str, i) { return ("" + (1 + i)).padStart(4, "0") + ": " + str }).join(' ')) 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 }
Обычный 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 < 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.offsetLeft*z) / c.width / z let y = (e.clientY + d.scrollTop - c.offsetTop*z) / 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) > 1e-7 && requestAnimationFrame(animate) } })() filter.ready = function() { let c = filter.canvas document.body.append(c) let z = window.getComputedStyle(c).zoom changeCenter({ clientX: c.width/2*z, clientY: c.height/2*z, }) 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% } 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(' ').map(function (str, i) { return ("" + (1 + i)).padStart(4, "0") + ": " + str }).join(' ')) 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 }