← Модули · Тильда · Zero Block

Неоновые кольца прогресса для Zero Block

Бесплатно

Zero Block с классом uc-neon-block. У круглого Shape — бордер в цвет неона и data-bg / data-rd / data-pc / data-speed. Текст с классом animate — scramble на время анимации кольца.

Обложка модуля: Неоновые кольца прогресса для Zero Block

Файлы

tilda-neon-rings.js — SVG-дуга + glow только на progress

Инструкция

  1. На Zero Block класс: uc-neon-block.
  2. Круглый Shape с бордером в цвет неона (толщина бордера = толщина кольца).
  3. DATA ATTRIBUTE на Shape:
  4. • data-bg — % прозрачности цвета бордера для трека.
  5. • data-rd — радиус свечения заполненной дуги (px).
  6. • data-pc — % заполнения круга.
  7. • data-speed — длительность анимации в секундах.
  8. Текст: класс animate.
  9. T123 — код целиком (с <script>). Несколько колец: каждое стартует, когда попадает в экран.

Код

<script>
/* ============================================
   YM Neon Rings для Тильды Zero Block
   Блок: uc-neon-block
   Shape: data-bg / data-rd / data-pc / data-speed
   Текст .animate — scramble на время data-speed
   https://ymportfolio.ru/modules/tilda-neon-rings/
   ============================================ */

(function () {
  var CFG = {
    block: "uc-neon-block",
    animate: "animate",
    bg: 10,
    rd: 30,
    pc: 0,
    speed: 1.15,
    scrambleChars:
      "ABCDEFGHJKLMNPQRSTUVWXYZАБВГДЕЖЗИКЛМНОПРСТУФХЦЧШЩЫЭЮЯ0123456789#%*+<>"
  };

  var css = [
    ".ym-neon-host{position:relative!important;}",
    ".ym-neon-host.ym-neon-ready > .tn-atom{",
    "  border-color:transparent!important;box-shadow:none!important;",
    "  -webkit-box-shadow:none!important;}",
    ".ym-neon-svg{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) rotate(-90deg);",
    "  width:100%;height:100%;overflow:visible;pointer-events:none;z-index:2;}",
    ".ym-neon-svg circle{fill:none;stroke-linecap:round;}",
    ".ym-neon-svg .ym-neon-track{transition:none;}",
    ".ym-neon-svg .ym-neon-glow,.ym-neon-svg .ym-neon-bar{",
    "  stroke-dashoffset:var(--ym-neon-len);",
    "  transition:stroke-dashoffset var(--ym-neon-dur,1.15s) cubic-bezier(.2,.8,.2,1);}",
    ".ym-neon-host.is-on .ym-neon-glow,.ym-neon-host.is-on .ym-neon-bar{",
    "  stroke-dashoffset:var(--ym-neon-off);}"
  ].join("");

  function injectCss() {
    if (document.getElementById("ym-neon-rings-css")) return;
    var s = document.createElement("style");
    s.id = "ym-neon-rings-css";
    s.textContent = css;
    document.head.appendChild(s);
  }

  function num(v, fallback) {
    var n = parseFloat(String(v == null ? "" : v).replace(",", "."));
    return isNaN(n) ? fallback : n;
  }

  function attr(el, name, fallback) {
    var atom = el.querySelector && el.querySelector(".tn-atom");
    var raw = el.getAttribute(name) || (atom && atom.getAttribute(name)) || "";
    return num(raw, fallback);
  }

  function parseRgb(color) {
    if (!color) return { r: 255, g: 255, b: 255 };
    color = String(color).trim();
    if (color.charAt(0) === "#") {
      var h = color.slice(1);
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
      var n = parseInt(h, 16);
      if (isNaN(n)) return { r: 255, g: 255, b: 255 };
      return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
    }
    var m = color.match(/rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
    if (m) return { r: +m[1], g: +m[2], b: +m[3] };
    return { r: 255, g: 255, b: 255 };
  }

  function rgba(c, a) {
    return "rgba(" + c.r + "," + c.g + "," + c.b + "," + a + ")";
  }

  function findBlocks() {
    var name = CFG.block.replace(/^\./, "");
    return Array.prototype.slice.call(
      document.querySelectorAll("." + name + ", .uc-" + name)
    );
  }

  function findRings(block) {
    var out = [];
    var seen = [];
    Array.prototype.forEach.call(block.querySelectorAll("[data-pc]"), function (node) {
      var el =
        node.classList && node.classList.contains("tn-elem")
          ? node
          : node.closest(".tn-elem") || node;
      if (!el || seen.indexOf(el) !== -1) return;
      seen.push(el);
      out.push(el);
    });
    return out;
  }

  function findAnimate(block) {
    var name = (CFG.animate || "animate").replace(/^\./, "");
    var nodes = block.querySelectorAll("." + name + ", .uc-" + name);
    var out = [];
    var seen = [];
    Array.prototype.forEach.call(nodes, function (node) {
      var el =
        node.classList && node.classList.contains("tn-elem")
          ? node
          : node.closest(".tn-elem") || node;
      if (!el || seen.indexOf(el) !== -1) return;
      seen.push(el);
      out.push(el);
    });
    return out;
  }

  function textRoot(el) {
    return (el && el.querySelector && el.querySelector(".tn-atom")) || el;
  }

  function collectTextNodes(root) {
    var list = [];
    function walk(n) {
      if (!n) return;
      if (n.nodeType === 3) {
        list.push(n);
        return;
      }
      if (n.nodeType !== 1) return;
      var kids = n.childNodes;
      for (var i = 0; i < kids.length; i++) walk(kids[i]);
    }
    walk(root);
    return list;
  }

  function randChar() {
    var set = CFG.scrambleChars || "ABCabc123#*";
    return set.charAt((Math.random() * set.length) | 0);
  }

  function ringSpeed(el) {
    var s = attr(el, "data-speed", CFG.speed);
    if (s <= 0) s = CFG.speed;
    if (s > 20) s = s / 1000;
    return s;
  }

  function scrambleText(el, durationSec) {
    var root = textRoot(el);
    if (!root || root.getAttribute("data-ym-scramble") === "1") return;
    root.setAttribute("data-ym-scramble", "1");

    var nodes = collectTextNodes(root).filter(function (n) {
      return n.nodeValue && n.nodeValue.length;
    });
    if (!nodes.length) return;

    var originals = nodes.map(function (n) {
      return n.nodeValue;
    });
    var dur = Math.max(0.2, durationSec || CFG.speed || 1.15) * 1000;
    var t0 = null;

    function paint(p) {
      nodes.forEach(function (node, idx) {
        var orig = originals[idx];
        var out = "";
        var len = orig.length || 1;
        for (var i = 0; i < orig.length; i++) {
          var ch = orig.charAt(i);
          if (/\s/.test(ch)) {
            out += ch;
            continue;
          }
          var settle = (i + 0.65) / len;
          if (p >= settle) out += ch;
          else if (p > settle - 0.18) out += Math.random() > 0.35 ? ch : randChar();
          else out += randChar();
        }
        node.nodeValue = out;
      });
    }

    paint(0);

    function frame(now) {
      if (t0 == null) t0 = now;
      var p = Math.min(1, (now - t0) / dur);
      paint(1 - Math.pow(1 - p, 2.2));
      if (p < 1) requestAnimationFrame(frame);
      else {
        nodes.forEach(function (node, idx) {
          node.nodeValue = originals[idx];
        });
      }
    }

    requestAnimationFrame(frame);
  }

  function setupRing(el) {
    if (el.getAttribute("data-ym-neon") === "1") return;
    var atom = el.querySelector(".tn-atom") || el;
    var pc = Math.max(0, Math.min(100, attr(el, "data-pc", CFG.pc)));
    var rd = Math.max(0, attr(el, "data-rd", CFG.rd));
    var bgPct = Math.max(0, Math.min(100, attr(el, "data-bg", CFG.bg)));
    var speed = ringSpeed(el);
    var trackA = bgPct / 100;

    var cs = window.getComputedStyle(atom);
    var borderColor = cs.borderTopColor || cs.borderColor || "#fff";
    var rgb = parseRgb(borderColor);
    var bw = parseFloat(cs.borderTopWidth || cs.borderWidth) || 4;
    if (bw < 1) bw = 4;

    var w = atom.offsetWidth || el.offsetWidth;
    var h = atom.offsetHeight || el.offsetHeight;
    var size = Math.min(w, h);
    if (size < 8) return;

    el.setAttribute("data-ym-neon", "1");
    el.setAttribute("data-ym-neon-speed", String(speed));
    el.classList.add("ym-neon-host");
    if (window.getComputedStyle(el).position === "static") {
      el.style.position = "relative";
    }

    atom.style.boxShadow = "none";
    atom.style.webkitBoxShadow = "none";
    el.classList.add("ym-neon-ready");

    var old = el.querySelector(".ym-neon-svg");
    if (old) old.remove();

    var r = Math.max(1, (size - bw) / 2);
    var c = 2 * Math.PI * r;
    var off = c * (1 - pc / 100);

    var svgNS = "http://www.w3.org/2000/svg";
    var svg = document.createElementNS(svgNS, "svg");
    svg.setAttribute("class", "ym-neon-svg");
    svg.setAttribute("viewBox", "0 0 " + size + " " + size);
    svg.setAttribute("aria-hidden", "true");
    svg.style.setProperty("--ym-neon-len", c.toFixed(2));
    svg.style.setProperty("--ym-neon-off", off.toFixed(2));
    svg.style.setProperty("--ym-neon-dur", speed + "s");

    function circle(cls, width, stroke, extra) {
      var n = document.createElementNS(svgNS, "circle");
      n.setAttribute("class", cls);
      n.setAttribute("cx", String(size / 2));
      n.setAttribute("cy", String(size / 2));
      n.setAttribute("r", String(r));
      n.setAttribute("stroke-width", String(width));
      n.setAttribute("stroke", stroke);
      n.style.strokeDasharray = c.toFixed(2);
      if (extra) {
        Object.keys(extra).forEach(function (k) {
          n.style[k] = extra[k];
        });
      }
      svg.appendChild(n);
      return n;
    }

    circle("ym-neon-track", bw, rgba(rgb, trackA), {
      strokeDasharray: "none",
      strokeDashoffset: "0"
    });

    var glowW = bw + Math.min(rd, 24) * 0.35;
    var glowBlur = Math.max(2, rd * 0.45);
    circle("ym-neon-glow", glowW, rgba(rgb, 0.75), {
      filter: "blur(" + glowBlur + "px)",
      opacity: "0.85"
    });

    circle("ym-neon-bar", bw, rgba(rgb, 1), {
      filter:
        "drop-shadow(0 0 " +
        rd +
        "px " +
        rgba(rgb, 0.95) +
        ") drop-shadow(0 0 " +
        rd * 0.45 +
        "px " +
        rgba(rgb, 0.7) +
        ")"
    });

    el.appendChild(svg);
  }

  function dist2(a, b) {
    var ra = a.getBoundingClientRect();
    var rb = b.getBoundingClientRect();
    var dx = (ra.left + ra.right) / 2 - (rb.left + rb.right) / 2;
    var dy = (ra.top + ra.bottom) / 2 - (rb.top + rb.bottom) / 2;
    return dx * dx + dy * dy;
  }

  function animatesForRing(ring, block) {
    var rings = findRings(block);
    var anims = findAnimate(block);
    if (!anims.length) return [];
    if (rings.length <= 1) return anims;

    return anims.filter(function (anim) {
      var nearest = rings[0];
      var best = dist2(anim, nearest);
      for (var i = 1; i < rings.length; i++) {
        var d = dist2(anim, rings[i]);
        if (d < best) {
          best = d;
          nearest = rings[i];
        }
      }
      return nearest === ring;
    });
  }

  function playRing(ring, block) {
    if (!ring || ring.getAttribute("data-ym-neon-on") === "1") return;
    ring.setAttribute("data-ym-neon-on", "1");

    if (ring.getAttribute("data-ym-neon") !== "1") setupRing(ring);
    if (ring.getAttribute("data-ym-neon") !== "1") return;

    ring.classList.add("is-on");
    var speed = num(ring.getAttribute("data-ym-neon-speed"), ringSpeed(ring));
    animatesForRing(ring, block).forEach(function (el) {
      scrambleText(el, speed);
    });
  }

  function observeRing(ring, block) {
    function go() {
      playRing(ring, block);
    }

    if (!("IntersectionObserver" in window)) {
      go();
      return;
    }

    var io = new IntersectionObserver(
      function (entries) {
        entries.forEach(function (e) {
          if (e.isIntersecting) {
            go();
            io.unobserve(e.target);
          }
        });
      },
      { threshold: 0.35, rootMargin: "0px 0px -8% 0px" }
    );
    io.observe(ring);
  }

  function init() {
    injectCss();
    var blocks = findBlocks();
    if (!blocks.length) return;
    blocks.forEach(function (block) {
      var rings = findRings(block);
      rings.forEach(function (ring) {
        setupRing(ring);
        observeRing(ring, block);
      });
    });
  }

  var tries = 0;
  function boot() {
    var ready = findBlocks().some(function (b) {
      return findRings(b).length > 0 || findAnimate(b).length > 0;
    });
    if (!ready && tries < 40) {
      tries += 1;
      setTimeout(boot, 120);
      return;
    }
    init();
  }

  function start() {
    if (typeof t_onFuncLoad === "function") {
      t_onFuncLoad("t396_init", function () {
        setTimeout(boot, 80);
      });
    } else {
      boot();
    }
  }

  if (typeof t_onReady === "function") {
    t_onReady(start);
  } else if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", start);
  } else {
    start();
  }

  window.YM_NEON_RINGS = CFG;
})();
</script>