Переключатель слайдеров, что больше идет на слайд шоу, но только под кнопками, что отлично смотрится при любой презентации материалов и так далее. Давайте начнем с создания нового экземпляра, где по центру выводим изображение, что изначально выставляем ширину по умолчанию. Также нужно сказать, что на мобильном аппарате корректно выводятся картинки, что не мало важно. Как уже сказано, что это больше похоже на презентацию предметов, чем на полноценный слайдер. Ведь здесь мы может переключатели вывести как на саму картинку, что ниже сделано. Так и под нее, где расположится на фоне. Здесь нет не каких каруселей, и нам не нужно подключать библиотеки. Ведь HTML-элемент, который будет привязан к стилистике, что самостоятельно можно все как нужно оформить. Так реально смотрится на полном экране: Установка: HTML Код
<image-slider index="1" animation-delay="1500"> <ul slot="images"> <li><img src="https://placekitten.com/512/512?image=1" alt="a cute kitten"></li> <li><img src="https://placekitten.com/512/512?image=2" alt="another cute kitten"></li> <li><img src="https://placekitten.com/512/512?image=3" alt="such a cute kitten"></li> <li><img src="https://placekitten.com/512/512?image=4" alt="an even more cute kitten"></li> <li><img src="https://placekitten.com/512/512?image=5" alt="another even more cute kitten"></li> <li><img src="https://placekitten.com/512/512?image=6" alt="super cute kitten"></li> <li><img src="https://placekitten.com/512/512?image=7" alt="super dooper cute kitten"></li> <li><img src="https://placekitten.com/512/512?image=8" alt="awww such a cute kitten"></li> <li><img src="https://placekitten.com/512/512?image=9" alt="kitten"></li> </ul> </image-slider> <nav> <ul> <li><a href="#image1" class="active">1</a></li> <li><a href="#image2">2</a></li> <li><a href="#image3">3</a></li> <li><a href="#image4">4</a></li> <li><a href="#image5">5</a></li> <li><a href="#image6">6</a></li> <li><a href="#image7">7</a></li> <li><a href="#image8">8</a></li> <li><a href="#image9">9</a></li> </ul> </nav>
CSS Код
image-slider { position: absolute; top: 0; left: 0; display: flex; width: 100vw; height: 100vh; justify-content: center; align-items: center; } image-slider ul { list-style: none; padding: 0; margin: 0; } image-slider ul li { margin: 0; padding: 0; } image-slider ul li img { display: block; width: 100vmin; height: 100vmin; } image-slider:not([index]) ul li:not(:first-child), image-slider[index="1"] ul li:not(:nth-child(1)), image-slider[index="2"] ul li:not(:nth-child(2)), image-slider[index="3"] ul li:not(:nth-child(3)), image-slider[index="4"] ul li:not(:nth-child(4)), image-slider[index="5"] ul li:not(:nth-child(5)), image-slider[index="6"] ul li:not(:nth-child(6)), image-slider[index="7"] ul li:not(:nth-child(7)), image-slider[index="8"] ul li:not(:nth-child(8)), image-slider[index="9"] ul li:not(:nth-child(9)) { display: none; } nav { position: absolute; bottom: 16px; left: 0; right: 0; display: block; } nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap-reverse; justify-content: center; } nav ul li { padding: 0; margin: 16px; } nav ul li a { font-variant-numeric: tabular-nums; display: block; padding: 8px; width: 40px; font-size: 16px; border: 2px solid #fff; background: #127; text-align: center; border-radius: 50%; color: #fff; text-decoration: none; font-weight: bold; } nav ul li a.active { background: #fff; color: #127; border: 2px solid #fff; }
JS
Код
function shader(code, shaderType) { return gl => { const sh = gl.createShader( /frag/.test(shaderType) ? gl.FRAGMENT_SHADER : gl.VERTEX_SHADER ); gl.shaderSource(sh, code); gl.compileShader(sh); if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { throw "Could not compile Shader.\n\n" + gl.getShaderInfoLog(sh); } return sh; } } function convertArray(data = [], type = WebGLRenderingContext.FLOAT) { if (type === WebGLRenderingContext.FLOAT) { return new Float32Array(data); } if (type === WebGLRenderingContext.BYTE) { return new Uint8Array(data); } return data; } class GLea { constructor({ canvas, gl, contextType = 'webgl', shaders, buffers, devicePixelRatio = 1, glOptions }) { this.canvas = canvas || document.querySelector('canvas'); this.gl = gl; if (!this.gl && this.canvas) { if (contextType === 'webgl') { this.gl = this.canvas.getContext('webgl', glOptions) || this.canvas.getContext('experimental-webgl', glOptions); } else { this.gl = this.canvas.getContext(contextType, glOptions); } if (! this.gl) { throw Error(`no ${contextType} context available.`) } } this.shaders = shaders; this.buffers = buffers; this.textures = []; this.devicePixelRatio = devicePixelRatio; } static vertexShader(code) { return gl => shader(code, 'vertex')(gl); } static fragmentShader(code) { return gl => shader(code, 'fragment')(gl); } static buffer(size, data, usage = WebGLRenderingContext.STATIC_DRAW, type = WebGLRenderingContext.FLOAT, normalized = false, stride = 0, offset = 0) { return (name, gl, program) => { const loc = gl.getAttribLocation(program, name); gl.enableVertexAttribArray(loc); // create buffer: const id = gl.createBuffer(); const bufferData = data instanceof Array ? convertArray(data, type) : data; gl.bindBuffer(gl.ARRAY_BUFFER, id); gl.bufferData(gl.ARRAY_BUFFER, bufferData, usage); gl.vertexAttribPointer( loc, size, type, normalized, stride, offset ); return { id, name, data: bufferData, loc, type, size } } } create() { const { gl, shaders } = this; this.program = gl.createProgram(); const { program } = this; shaders.map(shaderFunc => shaderFunc(gl)).map(shader => { gl.attachShader(program, shader); }); gl.linkProgram(program); gl.validateProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { const info = gl.getProgramInfoLog(program); throw "Could not compile WebGL program. \n\n" + info; } this.use(); Object.keys(this.buffers).forEach(name => { const bufferFunc = this.buffers[name]; this.buffers[name] = bufferFunc(name, gl, program); }); this.resize(); return this; } setActiveTexture(textureIndex, texture) { const { gl } = this; gl.activeTexture(gl['TEXTURE' + textureIndex.toString()]); gl.bindTexture(gl.TEXTURE_2D, texture); } createTexture(textureIndex = 0, params = { textureWrapS: 'clampToEdge', textureWrapT: 'clampToEdge', textureMinFilter: 'nearest', textureMagFilter: 'nearest' }) { const scream = (str = "") => (/^[A-Z0-9_]+$/.test(str) ? str : str.replace(/([A-Z])/g, '_$1').toUpperCase()); const { gl } = this; const texture = gl.createTexture(); gl.activeTexture(gl['TEXTURE' + textureIndex.toString()]); gl.bindTexture(gl.TEXTURE_2D, texture); for (let key in params) { if (params.hasOwnProperty(key)) { const KEY = scream(key); const VAL = scream(params[key]); if (KEY in gl && VAL in gl) { gl.texParameteri(gl.TEXTURE_2D, gl[KEY], gl[VAL]); } } } this.textures.push(texture); return texture; } updateBuffer(name, offset = 0) { const buffer = this.buffers[name]; gl.bindBuffer(gl.ARRAY_BUFFER, buffer.id); gl.bufferSubData(gl.ARRAY_BUFFER, offset, buffer.data); } resize() { const { canvas, gl, devicePixelRatio } = this; if (canvas) { canvas.width = canvas.clientWidth * devicePixelRatio; canvas.height = canvas.clientHeight * devicePixelRatio; gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); } } get width() { return this.canvas ? this.canvas.width : null; } get height() { return this.canvas ? this.canvas.height : null; } use() { this.gl.useProgram(this.program); return this; } uniM(name, data) { const { gl, program } = this; const { sqrt } = Math; const loc = gl.getUniformLocation(program, name); gl["uniformMatrix" + sqrt(data.length) + "fv"](loc, false, new Float32Array(data)); return loc; } uniV(name, data) { const { gl, program } = this; const loc = gl.getUniformLocation(program, name); gl["uniform" + data.length + "fv"](loc, (data instanceof Array || data instanceof Float32Array) ? data : new Float32Array(data)); return loc; } uniIV(name, data) { const { gl, program } = this; const loc = gl.getUniformLocation(program, name); gl["uniform" + data.length + "iv"](loc, new Int32Array(data)); return loc; } uni(name, data) { const { gl, program } = this; const loc = gl.getUniformLocation(program, name); if(typeof data === 'number') { gl.uniform1f(loc, data); } return loc; } uniI(name, data) { const { gl, program } = this; const loc = gl.getUniformLocation(program, name); if (typeof data === 'number') { gl.uniform1i(loc, data); } } clear(clearColor = null) { const { gl } = this; if (clearColor) { gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1); } gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) } destroy() { const { gl, program, canvas } = this; try { gl.deleteProgram(program); Object.values(this.buffers).forEach(buffer => { gl.deleteBuffer(buffer.id); }); this.textures.forEach(texture => { gl.deleteTexture(texture); }) gl.getExtension('WEBGL_lose_context').loseContext(); const newCanvas = canvas.cloneNode(); canvas.style.display = 'none'; if (canvas.parentNode) { canvas.parentNode.insertBefore(newCanvas, canvas); canvas.parentNode.removeChild(canvas); } this.gl = null; this.canvas = newCanvas; this.program = null; } catch (err) { console.error(err); } } } const clamp = (value, min, max) => Math.min(Math.max(value, min), max); const html = x => x; function easeInOutCubic(t) { return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; } const glsl = x => x[0].trim(); const vert = glsl` precision highp float; attribute vec2 position; uniform float time; uniform float width; uniform float height; void main() { gl_Position = vec4(position, 0.0, 1.0); } `; const frag = glsl` precision highp float; uniform float width; uniform float height; uniform float time; uniform float animationStep; uniform sampler2D texture1; uniform sampler2D texture2; uniform ivec2 textureSize1; uniform ivec2 textureSize2; // normalize coords and correct for aspect ratio vec2 normalizeScreenCoords() { float aspectRatio = width / height; vec2 result = 2.0 * (gl_FragCoord.xy / vec2(width, height) - 0.5); result.x *= aspectRatio; return result; } vec4 invert(vec4 color) { return vec4(1.0 - color.x, 1.0 - color.y, 1.0 - color.z, 1.0); } float rand() { return fract(sin(dot(gl_FragCoord.xy + sin(time),vec2(12.9898,78.233))) * 43758.5453); } void main() { vec2 p = normalizeScreenCoords(); // float x = .5 + .5 * sin(time * .25); float x = clamp(animationStep, 0.0, 1.0); float y = 1.0 - x; float deform = rand() * .04 + sin(time * 1.2 + p.x * 11.0 - p.y * sin(p.x * 2.0) * 13.0) * .01; vec2 texCoords = vec2(gl_FragCoord.x / width, 1.0 - (gl_FragCoord.y / height)); vec4 tex1Color = texture2D(texture1, texCoords + x * deform); vec4 tex2Color = texture2D(texture2, texCoords + y * deform); gl_FragColor = mix(tex1Color, tex2Color, x); } `; class ImageSlider extends HTMLElement { constructor() { super(); this._animationDelay = 1000; this.imageContainer = this.querySelector('[slot]'); this.animationLoop = this.animationLoop.bind(this); this.onContextLost = this.onContextLost.bind(this); this.onContextRestored = this.onContextRestored.bind(this); this.attachShadow({ mode: 'open' }); this.initialized = false; this.prevIndex = 1; this.indexChangedTime = 0; } static register() { try { customElements.define('image-slider', ImageSlider); } catch (ex) { console.error('Custom elements are not supported.') } } static get observedAttributes() { return ['index', 'autoplay', 'animation-delay']; } get animationDelay() { return _animationDelay; } get autoplay() { return this.hasAttribute('autoplay'); } set autoplay(value) { if (Boolean(value) === true) { this.setAttribute('autoplay', 'autoplay'); } else { this.removeAttribute('autoplay'); } } get index() { if (this.hasAttribute('index')) { const imageIndex = parseInt(this.getAttribute('index') || '1', 10); return isNaN(imageIndex) ? 1 : imageIndex; } return null; } set index(value) { if (value === null || typeof value === 'undefined') { this.removeAttribute('index'); return; } if (typeof value !== 'number') { value = clamp(parseInt(value, 10), 1, this.images.length); if (isNaN(value)) { value = 1; } } this.setAttribute('index', value.toString(10)); } /** * get fading state * @returns {boolean} true if fading */ get fading() { if (this.sameTextures) return false; return performance.now() - this.indexChangedTime < this._animationDelay; } attributeChangedCallback(name, oldValue, newValue) { if (name === 'index' && oldValue !== null && oldValue !== newValue) { this.indexChangedTime = performance.now(); const prev = parseInt(oldValue || '1', 10); this.prevIndex = isNaN(prev) ? 1 : prev; this.updateTextures(); } if (name === 'animation-delay') { const returnValue = parseInt(newValue, 10); this._animationDelay = isNaN(returnValue) ? 1000 : returnValue; } } async loadImages() { const imgs = [...this.imageContainer.querySelectorAll('img')]; const returnValue = await Promise.all(imgs.map(img => { return new Promise((resolve, reject) => { const image = new Image(); image.crossOrigin = 'anonymous'; image.src = img.src; image.onload = () => resolve(image); image.onerror = reject; }) })); this.indexChangedTime = performance.now(); return returnValue; } async loadCss() { const css = ` *, *::before, *::after { box-sizing: border-box; } :host([hidden]) { display: none; } :host { display: flex; width: 100vw; height: 100vh; background: #000; justify-content: center; align-items: center; } canvas { margin: auto; width: 100vmin; height: 100vmin; } button { display: none; } ::slotted(*) { display: none; } `; return '<style>' + css + '</style>'; } /** * called when the component is attached to the dom */ async connectedCallback() { if (! this.initialized) { this.initialized = true; this.css = await this.loadCss(); this.images = await this.loadImages(); this.render(); this.canvas = this.shadowRoot.querySelector('canvas'); this.initWebGL(); this.canvas.addEventListener('webglcontextlost', this.onContextLost); this.canvas.addEventListener('webglcontextrestored', this.onContextRestored); } } disconnectedCallback() { cancelAnimationFrame(this.frame); this.canvas.removeEventListener('webglcontextlost', this.onContextLost); this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored); this.glea.destroy(); this.initialized = false; } initWebGL() { this.glea = new GLea({ canvas: this.canvas, shaders: [ GLea.fragmentShader(frag), GLea.vertexShader(vert) ], buffers: { 'position': GLea.buffer(2, [1, 1, -1, 1, 1,-1, -1,-1]) } }).create(); const { glea } = this; const { gl } = glea; const image1 = this.images[this.index - 1]; const image2 = this.images[this.index - 1]; this.sameTextures = true; this.texture1 = glea.createTexture(0); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image1); this.texture2 = glea.createTexture(1); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image2); glea.uniI('texture1', 0); glea.uniI('texture2', 1); glea.uniIV('textureSize1', [image1.width, image1.height]); glea.uniIV('textureSize2', [image2.width, image2.height]); glea.uni('animationStep', 0); window.addEventListener('resize', () => { glea.resize(); }); this.animationLoop(); } /** * upload images to WebGL textures */ updateTextures() { const { glea, images, index, prevIndex } = this; const { gl } = glea; if (this.texture1 && this.texture2) { const image1 = images[prevIndex - 1]; const image2 = images[index - 1]; this.sameTextures = (prevIndex === index); if (image1 && image1.complete) { glea.setActiveTexture(0, this.texture1); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, image1); } if (image2 && image2.complete) { glea.setActiveTexture(1, this.texture2); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, image2); } glea.uni('animationStep', 0); } } animationLoop(time = 0) { const { glea } = this; const { gl } = glea; const animationTime = (performance.now() - this.indexChangedTime) / this._animationDelay; if (animationTime <= 1) { const animationStep = this.sameTextures ? 0 : easeInOutCubic(clamp(animationTime, 0, 1)); glea.clear(); glea.uni('width', glea.width); glea.uni('height', glea.height); glea.uni('time', time * .005); glea.uni('animationStep', animationStep); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } this.frame = requestAnimationFrame(this.animationLoop); } onContextLost(e) { e.preventDefault(); cancelAnimationFrame(this.frame); } onContextRestored() { this.initWebGL(); } render() { const { css } = this; this.shadowRoot.innerHTML = css + html` <canvas></canvas> <slot name="images"></slot> `; } } ImageSlider.register(); const nav = document.querySelector('nav'); const imageSlider = document.querySelector('image-slider'); nav.addEventListener('click', (e) => { e.preventDefault(); const active = nav.querySelector('.active'); if (active) { active.classList.remove('active'); } if (e.target.nodeName === 'A') { const a = e.target; const newIndex = a.getAttribute('href').slice(-1); a.classList.add('active'); imageSlider.setAttribute('index', newIndex); } }); function prevImage() { const active = nav.querySelector('.active'); if (active) { active.classList.remove('active'); } const currentIndex = parseInt(imageSlider.getAttribute('index'), 10) || 1; const numImages = [...imageSlider.querySelectorAll('img')].length; const newIndex = (currentIndex > 1) ? (currentIndex-1) : numImages; document.querySelector(`[href="#image${newIndex}"]`).classList.add('active'); imageSlider.setAttribute('index', newIndex); } function nextImage() { const active = nav.querySelector('.active'); if (active) { active.classList.remove('active'); } const currentIndex = parseInt(imageSlider.getAttribute('index'), 10) || 1; const numImages = [...imageSlider.querySelectorAll('img')].length; const newIndex = (currentIndex < numImages) ? (currentIndex + 1) : 1; document.querySelector(`[href="#image${newIndex}"]`).classList.add('active'); imageSlider.setAttribute('index', newIndex); } window.addEventListener('keyup', (e) => { if (imageSlider.fading === true) { return; } if (e.keyCode === 37) { prevImage(); } if (e.keyCode === 39) { nextImage(); } });
Плоскость будет копировать свои размеры CSS и позиции, где при изменении размера окна, оно будет обновляться до новых размеров. Здесь также можно создать текстуру для всех изображений, холстов и видео дочерних элементов этого элемента, в нашем случае у нас просто одно изображение. Демонстрация