/* ============================================================================ * SoftSPM — animowane tlo "flowing wave mesh" (Canvas 2D, bez bibliotek) * ---------------------------------------------------------------------------- * Tryby: * 'text' - wezly ukladaja sie w podany napis (drgaja z mala amplituda), * 'wave' - pasmo falujacych linii (suma sinusow, ruch organiczny). * * Sterowanie z HTML przez atrybuty canvasu: * data-mode="text|wave" data-text="SoftSPM" * * Wszystkie parametry w jednym obiekcie CONFIG ponizej. * Zgodne z PHP 5.4 / starymi przegladarkami (czysty ES5, requestAnimationFrame). * ========================================================================== */ (function () { 'use strict'; var CONFIG = { mode: 'text', // 'text' | 'wave' text: 'SoftSPM', // --- siatka / probkowanie --- grid: 7, // odleglosc miedzy wezlami [px] (mniej = gesciej) alphaThreshold: 120, // min. alfa piksela litery, by postawic wezel // --- tekst --- fontWeight: '700', fontFamily: '"Segoe UI", Roboto, Arial, sans-serif', textHeightRatio: 0.17, // wysokosc fontu wzgledem wysokosci canvasu textAnchor: { x: 0.07, y: 0.90 }, // kotwica napisu (ulamek szer./wys.) -> dol-lewo // --- ruch --- amplitude: 1.7, // amplituda drgania wezlow [px] (maly = czytelny napis) speed: 0.0016, // mnoznik czasu // --- kolor / poswiata --- color: { r: 41, g: 171, b: 226 }, // turkus marki SoftSPM (rgb 41,171,226) opacity: 0.85, glow: 8, // shadowBlur (poswiata neonowa) lineWidth: 1, nodeRadius: 1.15, drawNodes: true, // --- reflektor (jasnosc wygasa przy krawedziach) --- edgeFalloff: 0.55, // 0 = brak, 1 = pelne wygaszenie na krawedziach spotlightCenter: null, // null = auto (srodek napisu / 0.5 dla fali) // --- tryb 'wave' --- waveCols: 90, waveRows: 13, bandCenter: 0.72, // srodek pasma (ulamek wysokosci) bandWidth: 0.40, // szerokosc pasma (ulamek wysokosci) connectorEvery: 3, // co ile kolumn pionowy lacznik (efekt siatki) // skladowe fal: [amplituda(px), czestotliwosc, predkosc, faza] waves: [ [22, 0.018, 0.9, 0.0], [14, 0.035, -1.4, 1.7], [9, 0.061, 2.1, 3.1], [6, 0.090, -2.8, 0.6] ] }; function MeshAnim(canvas, cfg) { this.canvas = canvas; this.cfg = cfg; this.ctx = canvas.getContext('2d'); this.dpr = window.devicePixelRatio || 1; this.w = 0; this.h = 0; this.nodes = []; this.edges = []; this.spotX = 0.5; this.raf = 0; this._resizeTimer = 0; var self = this; this._onResize = function () { clearTimeout(self._resizeTimer); self._resizeTimer = setTimeout(function () { self.resize(); }, 120); }; window.addEventListener('resize', this._onResize); } MeshAnim.prototype.resize = function () { var rect = this.canvas.getBoundingClientRect(); this.dpr = window.devicePixelRatio || 1; this.w = Math.max(1, Math.round(rect.width)); this.h = Math.max(1, Math.round(rect.height)); this.canvas.width = Math.round(this.w * this.dpr); this.canvas.height = Math.round(this.h * this.dpr); this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); this.build(); }; MeshAnim.prototype.build = function () { if (this.cfg.mode === 'wave') this.buildWave(); else this.buildText(); // srodek reflektora if (this.cfg.spotlightCenter != null) { this.spotX = this.cfg.spotlightCenter; } else if (this.cfg.mode === 'text' && this.nodes.length) { var minX = Infinity, maxX = -Infinity, i; for (i = 0; i < this.nodes.length; i++) { if (this.nodes[i].bx < minX) minX = this.nodes[i].bx; if (this.nodes[i].bx > maxX) maxX = this.nodes[i].bx; } this.spotX = ((minX + maxX) / 2) / this.w; } else { this.spotX = 0.5; } }; // --- model TEKST: render na ukrytym canvasie -> probkowanie pikseli --- MeshAnim.prototype.buildText = function () { var cfg = this.cfg, w = this.w, h = this.h; this.nodes = []; this.edges = []; if (!w || !h) return; var off = document.createElement('canvas'); off.width = w; off.height = h; var octx = off.getContext('2d'); var size = Math.round(h * cfg.textHeightRatio); octx.font = cfg.fontWeight + ' ' + size + 'px ' + cfg.fontFamily; octx.textBaseline = 'alphabetic'; octx.textAlign = 'left'; octx.fillStyle = '#fff'; octx.fillText(cfg.text, Math.round(w * cfg.textAnchor.x), Math.round(h * cfg.textAnchor.y)); var img = octx.getImageData(0, 0, w, h).data; var step = cfg.grid; var cols = Math.floor(w / step); var rows = Math.floor(h / step); var indexMap = {}; // "c_r" -> node index var c, r, px, py, a, idx; for (r = 0; r < rows; r++) { for (c = 0; c < cols; c++) { px = c * step + (step >> 1); py = r * step + (step >> 1); a = img[(py * w + px) * 4 + 3]; if (a >= cfg.alphaThreshold) { indexMap[c + '_' + r] = this.nodes.length; this.nodes.push({ bx: px, by: py, ph: (c * 0.7 + r * 1.3), // faza drgania fx: 0.9 + (c % 5) * 0.06, // lekko rozne czestotliwosci fy: 1.1 + (r % 4) * 0.05 }); } } } // krawedzie: prawo, dol, skos prawy-dol (siatka trojkatna) var neigh = [[1, 0], [0, 1], [1, 1]]; for (r = 0; r < rows; r++) { for (c = 0; c < cols; c++) { idx = indexMap[c + '_' + r]; if (idx == null) continue; for (var n = 0; n < neigh.length; n++) { var j = indexMap[(c + neigh[n][0]) + '_' + (r + neigh[n][1])]; if (j != null) this.edges.push([idx, j]); } } } }; // --- model FALA: pasmo wezlow, ruch = suma sinusow --- MeshAnim.prototype.buildWave = function () { var cfg = this.cfg, w = this.w, h = this.h; this.nodes = []; this.edges = []; if (!w || !h) return; var cols = cfg.waveCols, rows = cfg.waveRows; var band = cfg.bandWidth * h; var top = cfg.bandCenter * h - band / 2; var grid = []; var c, r; for (r = 0; r < rows; r++) { grid[r] = []; for (c = 0; c < cols; c++) { var bx = (cols === 1 ? 0 : c / (cols - 1)) * w; var by = top + (rows === 1 ? 0.5 : r / (rows - 1)) * band; grid[r][c] = this.nodes.length; this.nodes.push({ bx: bx, by: by, col: c, row: r }); } } for (r = 0; r < rows; r++) { for (c = 0; c < cols; c++) { if (c < cols - 1) this.edges.push([grid[r][c], grid[r][c + 1]]); // poziome if (r < rows - 1 && c % cfg.connectorEvery === 0) { this.edges.push([grid[r][c], grid[r + 1][c]]); // pionowe laczniki } } } }; MeshAnim.prototype.nodePos = function (nd, t) { var cfg = this.cfg; if (cfg.mode === 'wave') { var y = nd.by, wv = cfg.waves, k; for (k = 0; k < wv.length; k++) { y += wv[k][0] * Math.sin(nd.bx * wv[k][1] + t * wv[k][2] + wv[k][3] + nd.row * 0.4); } return [nd.bx, y]; } // tryb tekstu: drobne drganie (napis czytelny) var a = cfg.amplitude; return [ nd.bx + a * Math.sin(t * nd.fx + nd.ph), nd.by + a * Math.cos(t * nd.fy + nd.ph * 1.3) ]; }; MeshAnim.prototype.frame = function (now) { var self = this; var cfg = this.cfg, ctx = this.ctx, w = this.w, h = this.h; var t = now * cfg.speed; ctx.clearRect(0, 0, w, h); if (this.nodes.length) { // policz pozycje raz na klatke var pos = new Array(this.nodes.length), i; for (i = 0; i < this.nodes.length; i++) pos[i] = this.nodePos(this.nodes[i], t); var col = cfg.color; var stroke = 'rgb(' + col.r + ',' + col.g + ',' + col.b + ')'; ctx.lineWidth = cfg.lineWidth; ctx.lineCap = 'round'; ctx.shadowColor = stroke; ctx.shadowBlur = cfg.glow; ctx.strokeStyle = stroke; // kubelki jasnosci (reflektor) -> 1 stroke na kubelek = wydajnie z glow var BUCKETS = 6, b, paths = []; for (b = 0; b < BUCKETS; b++) paths[b] = []; var e, a, bxC, fac, bk; for (i = 0; i < this.edges.length; i++) { e = this.edges[i]; a = pos[e[0]]; var b2 = pos[e[1]]; bxC = ((a[0] + b2[0]) / 2) / w; // srodek krawedzi (ulamek szer.) fac = 1 - cfg.edgeFalloff * Math.pow(Math.abs(bxC - this.spotX) * 2, 2); if (fac <= 0.02) continue; bk = Math.min(BUCKETS - 1, Math.max(0, Math.floor(fac * BUCKETS))); paths[bk].push(a, b2); } for (b = 0; b < BUCKETS; b++) { if (!paths[b].length) continue; ctx.globalAlpha = cfg.opacity * ((b + 0.5) / BUCKETS); ctx.beginPath(); for (i = 0; i < paths[b].length; i += 2) { ctx.moveTo(paths[b][i][0], paths[b][i][1]); ctx.lineTo(paths[b][i + 1][0], paths[b][i + 1][1]); } ctx.stroke(); } // wezly jako drobne punkty (jeden fill) if (cfg.drawNodes) { ctx.globalAlpha = cfg.opacity; ctx.fillStyle = stroke; ctx.beginPath(); for (i = 0; i < pos.length; i++) { ctx.moveTo(pos[i][0] + cfg.nodeRadius, pos[i][1]); ctx.arc(pos[i][0], pos[i][1], cfg.nodeRadius, 0, Math.PI * 2); } ctx.fill(); } ctx.globalAlpha = 1; ctx.shadowBlur = 0; } this.raf = window.requestAnimationFrame(function (ts) { self.frame(ts); }); }; MeshAnim.prototype.start = function () { var self = this; this.resize(); this.raf = window.requestAnimationFrame(function (ts) { self.frame(ts); }); }; MeshAnim.prototype.destroy = function () { window.cancelAnimationFrame(this.raf); window.removeEventListener('resize', this._onResize); }; function boot() { var canvas = document.getElementById('spm-mesh'); if (!canvas) return; var cfg = {}; for (var k in CONFIG) if (CONFIG.hasOwnProperty(k)) cfg[k] = CONFIG[k]; if (canvas.getAttribute('data-mode')) cfg.mode = canvas.getAttribute('data-mode'); if (canvas.getAttribute('data-text')) cfg.text = canvas.getAttribute('data-text'); var anim = new MeshAnim(canvas, cfg); window.spmMesh = anim; // przy wlasnej czcionce poczekaj na gotowosc fontow (czytelny napis) if (cfg.mode === 'text' && document.fonts && document.fonts.ready) { document.fonts.ready.then(function () { anim.start(); }); } else { anim.start(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();