312 lines
12 KiB
JavaScript
312 lines
12 KiB
JavaScript
/* ============================================================================
|
|
* 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();
|
|
}
|
|
})();
|