Перейти к содержимому

Иллюстрации

Щит бесконечности

Анимированная изометрическая иллюстрация щита с символом бесконечности и эффектом пульсации.


Щит бесконечности

ShieldInfinity — изометрический щит с объёмным контуром бесконечности. Наведение усиливает пульсацию и создаёт эхо вокруг символа.

Интерактивное демо

Пропсы

Акцент
<script lang="ts">
  import { onMount } from 'svelte';

  type Pt = [number, number];

  /** Акцентный цвет трубки бесконечности. */
  export let accent = '#f43f5e';
  /** CSS-ширина всей фигуры. */
  export let width = 'min(92vw, 560px)';
  export let label = 'Isometric shield with infinity symbol';

  const D =
    'M0,-120 C40,-100 80,-95 100,-95 L100,-10 C100,60 55,100 0,125 C-55,100 -100,60 -100,-10 L-100,-95 C-80,-95 -40,-100 0,-120Z';
  const DEPTH = 14; // толщина щита (в локальных единицах)
  const H = 12; // толщина трубки бесконечности (в локальных единицах)
  const corners: Pt[] = [
    [0, -120],
    [100, -95],
    [0, 125],
    [-100, -95]
  ];

  // контур лемнискаты
  const M = 240;
  const pts: Pt[] = Array.from({ length: M + 1 }, (_, k): Pt => {
    const t = (k / M) * Math.PI * 2;
    const e = 1 + Math.sin(t) ** 2;
    return [(68 * Math.cos(t)) / e, (68 * Math.sin(t) * Math.cos(t)) / e];
  });
  const P = 'M' + pts.map((p) => p[0].toFixed(2) + ',' + p[1].toFixed(2)).join(' L') + 'Z';
  const slices = Array.from({ length: H }, (_, k) => {
    const i = H - k;
    return { i, mix: 25 + (1 - i / H) * 35 };
  });

  let front: SVGPathElement;
  let conn: Pt[] = []; // рёбра силуэта толщины щита
  let tgt = 0;
  let hov = 0;
  let ph = 0;
  let w = 7; // толщина обводки трубки, px
  let sc = 1; // масштаб пульсации трубки
  let ech = [
    { f: 0, o: 0 },
    { f: 0, o: 0 }
  ];

  onMount(() => {
    // рёбра, где контур идёт параллельно направлению глубины (только нижняя кривая)
    const len = front.getTotalLength();
    const N = 480;
    const out: Pt[] = [];
    let pv = 0;
    let pp: DOMPoint | null = null;
    for (let i = 0; i <= N; i++) {
      const q = front.getPointAtLength((len * i) / N);
      if (pp) {
        const d = q.x - pp.x + (q.y - pp.y);
        if (pv && d * pv < 0 && pp.y > 20) out.push([pp.x, pp.y]);
        if (d) pv = d;
      }
      pp = q;
    }
    conn = out;

    let raf = 0;
    let last = performance.now();
    const tick = (now: number): void => {
      const dt = Math.min(0.05, (now - last) / 1000);
      last = now;
      hov += (tgt - hov) * Math.min(1, dt * 8);
      ph += dt * hov * 1.5;
      const beat = Math.max(0, Math.sin(ph * Math.PI * 2)) ** 3;
      w = 7 + hov * (1 + 3 * beat);
      sc = 1 + hov * (0.04 + 0.07 * beat);
      ech = [0, 1].map((i) => {
        const f = (ph + (i + 1) / 2) % 1;
        return { f, o: (1 - f) * 0.55 * hov };
      });
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  });
</script>

<svg
  class="hl"
  viewBox="0 0 600 600"
  role="img"
  aria-label={label}
  style="--hl-accent:{accent}; width:{width}"
  on:pointerenter={() => (tgt = 1)}
  on:pointerleave={() => (tgt = 0)}
  on:pointerdown={() => (tgt = 1)}
  on:pointerup={(e) => {
    if (e.pointerType !== 'mouse') tgt = 0;
  }}
>
  <g transform="translate(300 300) rotate(8) scale(1.15) matrix(0.866 0.5 0 1 0 0)">
    <!-- щит: задняя грань, рёбра толщины, передняя грань (непрозрачная, скрывает то, что за ней) -->
    <path class="l bk" d={D} transform="translate({DEPTH} {-DEPTH})" />
    <g>
      {#each corners as c, i (i)}
        <line class="l" x1={c[0]} y1={c[1]} x2={c[0] + DEPTH} y2={c[1] - DEPTH} />
      {/each}
      {#each conn as c, i (i)}
        <line class="l" x1={c[0]} y1={c[1]} x2={c[0] + DEPTH} y2={c[1] - DEPTH} />
      {/each}
    </g>
    <path class="l fl" d={D} bind:this={front} />
    <path class="l f" d={D} transform="scale(.86) translate(0 4)" />

    <!-- бесконечность: выдавленная трубка -->
    <g transform="translate(-4 10)">
      <g transform="scale({sc})">
        {#each slices as s (s.i)}
          <path
            class="tube"
            d={P}
            transform="translate({s.i} {-s.i})"
            style="stroke:color-mix(in srgb, var(--hl-accent) {s.mix}%, var(--hl-bg)); stroke-width:{w}px"
          />
        {/each}
        <path class="tube" d={P} style="stroke:var(--hl-accent); stroke-width:{w}px" />
      </g>
      {#each ech as e, i (i)}
        <g transform="translate({e.f * H * 0.5} {-e.f * H * 0.5}) scale({1 + e.f * 0.4})">
          <path class="echo" d={P} style="stroke-opacity:{e.o}" />
        </g>
      {/each}
    </g>
  </g>
</svg>

<style>
  .hl {
    --hl-bg: #ffffff;
    --hl-line: #171717;
    --hl-faint: rgba(23, 23, 23, 0.22);
    display: block;
    height: auto;
    max-height: 80vh;
    cursor: pointer;
    touch-action: none;
  }

  :global(.dark) .hl,
  :global([data-theme='dark']) .hl {
    --hl-bg: #090909;
    --hl-line: #f5f5f5;
    --hl-faint: rgba(245, 245, 245, 0.28);
  }
  .l {
    fill: none;
    stroke: var(--hl-line);
    stroke-width: 1.2;
    stroke-linejoin: round;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }
  .f {
    stroke: var(--hl-faint);
  }
  .bk {
    stroke-opacity: 0.6;
  }
  .fl {
    fill: var(--hl-bg);
  }
  .tube {
    fill: none;
    stroke-linejoin: round;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }
  .echo {
    fill: none;
    stroke: var(--hl-accent);
    stroke-width: 1.4;
    vector-effect: non-scaling-stroke;
  }
</style>
<script lang="ts">
  import { onMount } from 'svelte';

  type Pt = [number, number];

  /** Акцентный цвет трубки бесконечности. */
  export let accent = '#f43f5e';
  /** CSS-ширина всей фигуры. */
  export let width = 'min(92vw, 560px)';
  export let label = 'Isometric shield with infinity symbol';

  const D =
    'M0,-120 C40,-100 80,-95 100,-95 L100,-10 C100,60 55,100 0,125 C-55,100 -100,60 -100,-10 L-100,-95 C-80,-95 -40,-100 0,-120Z';
  const DEPTH = 14; // толщина щита (в локальных единицах)
  const H = 12; // толщина трубки бесконечности (в локальных единицах)
  const corners: Pt[] = [
    [0, -120],
    [100, -95],
    [0, 125],
    [-100, -95]
  ];

  // контур лемнискаты
  const M = 240;
  const pts: Pt[] = Array.from({ length: M + 1 }, (_, k): Pt => {
    const t = (k / M) * Math.PI * 2;
    const e = 1 + Math.sin(t) ** 2;
    return [(68 * Math.cos(t)) / e, (68 * Math.sin(t) * Math.cos(t)) / e];
  });
  const P = 'M' + pts.map((p) => p[0].toFixed(2) + ',' + p[1].toFixed(2)).join(' L') + 'Z';
  const slices = Array.from({ length: H }, (_, k) => {
    const i = H - k;
    return { i, mix: 25 + (1 - i / H) * 35 };
  });

  let front: SVGPathElement;
  let conn: Pt[] = []; // рёбра силуэта толщины щита
  let tgt = 0;
  let hov = 0;
  let ph = 0;
  let w = 7; // толщина обводки трубки, px
  let sc = 1; // масштаб пульсации трубки
  let ech = [
    { f: 0, o: 0 },
    { f: 0, o: 0 }
  ];

  onMount(() => {
    // рёбра, где контур идёт параллельно направлению глубины (только нижняя кривая)
    const len = front.getTotalLength();
    const N = 480;
    const out: Pt[] = [];
    let pv = 0;
    let pp: DOMPoint | null = null;
    for (let i = 0; i <= N; i++) {
      const q = front.getPointAtLength((len * i) / N);
      if (pp) {
        const d = q.x - pp.x + (q.y - pp.y);
        if (pv && d * pv < 0 && pp.y > 20) out.push([pp.x, pp.y]);
        if (d) pv = d;
      }
      pp = q;
    }
    conn = out;

    let raf = 0;
    let last = performance.now();
    const tick = (now: number): void => {
      const dt = Math.min(0.05, (now - last) / 1000);
      last = now;
      hov += (tgt - hov) * Math.min(1, dt * 8);
      ph += dt * hov * 1.5;
      const beat = Math.max(0, Math.sin(ph * Math.PI * 2)) ** 3;
      w = 7 + hov * (1 + 3 * beat);
      sc = 1 + hov * (0.04 + 0.07 * beat);
      ech = [0, 1].map((i) => {
        const f = (ph + (i + 1) / 2) % 1;
        return { f, o: (1 - f) * 0.55 * hov };
      });
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  });
</script>

<svg
  class="hl"
  viewBox="0 0 600 600"
  role="img"
  aria-label={label}
  style="--hl-accent:{accent}; width:{width}"
  on:pointerenter={() => (tgt = 1)}
  on:pointerleave={() => (tgt = 0)}
  on:pointerdown={() => (tgt = 1)}
  on:pointerup={(e) => {
    if (e.pointerType !== 'mouse') tgt = 0;
  }}
>
  <g transform="translate(300 300) rotate(8) scale(1.15) matrix(0.866 0.5 0 1 0 0)">
    <!-- щит: задняя грань, рёбра толщины, передняя грань (непрозрачная, скрывает то, что за ней) -->
    <path class="l bk" d={D} transform="translate({DEPTH} {-DEPTH})" />
    <g>
      {#each corners as c, i (i)}
        <line class="l" x1={c[0]} y1={c[1]} x2={c[0] + DEPTH} y2={c[1] - DEPTH} />
      {/each}
      {#each conn as c, i (i)}
        <line class="l" x1={c[0]} y1={c[1]} x2={c[0] + DEPTH} y2={c[1] - DEPTH} />
      {/each}
    </g>
    <path class="l fl" d={D} bind:this={front} />
    <path class="l f" d={D} transform="scale(.86) translate(0 4)" />

    <!-- бесконечность: выдавленная трубка -->
    <g transform="translate(-4 10)">
      <g transform="scale({sc})">
        {#each slices as s (s.i)}
          <path
            class="tube"
            d={P}
            transform="translate({s.i} {-s.i})"
            style="stroke:color-mix(in srgb, var(--hl-accent) {s.mix}%, var(--hl-bg)); stroke-width:{w}px"
          />
        {/each}
        <path class="tube" d={P} style="stroke:var(--hl-accent); stroke-width:{w}px" />
      </g>
      {#each ech as e, i (i)}
        <g transform="translate({e.f * H * 0.5} {-e.f * H * 0.5}) scale({1 + e.f * 0.4})">
          <path class="echo" d={P} style="stroke-opacity:{e.o}" />
        </g>
      {/each}
    </g>
  </g>
</svg>

<style>
  .hl {
    --hl-bg: #ffffff;
    --hl-line: #171717;
    --hl-faint: rgba(23, 23, 23, 0.22);
    display: block;
    height: auto;
    max-height: 80vh;
    cursor: pointer;
    touch-action: none;
  }

  :global(.dark) .hl,
  :global([data-theme='dark']) .hl {
    --hl-bg: #090909;
    --hl-line: #f5f5f5;
    --hl-faint: rgba(245, 245, 245, 0.28);
  }
  .l {
    fill: none;
    stroke: var(--hl-line);
    stroke-width: 1.2;
    stroke-linejoin: round;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }
  .f {
    stroke: var(--hl-faint);
  }
  .bk {
    stroke-opacity: 0.6;
  }
  .fl {
    fill: var(--hl-bg);
  }
  .tube {
    fill: none;
    stroke-linejoin: round;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }
  .echo {
    fill: none;
    stroke: var(--hl-accent);
    stroke-width: 1.4;
    vector-effect: non-scaling-stroke;
  }
</style>

Использование

<script lang="ts">
	import ShieldInfinity from '$lib/components/ui-registry/ShieldInfinity.svelte';
</script>

<ShieldInfinity accent="#f43f5e" />
<script lang="ts">
	import ShieldInfinity from '$lib/components/ui-registry/ShieldInfinity.svelte';
</script>

<ShieldInfinity accent="#f43f5e" />

Пропсы

Пропс Тип По умолчанию Описание
accent string '#f43f5e' Цвет трубки бесконечности и акцентов
width string 'min(92vw, 560px)' CSS-ширина иллюстрации
label string 'Isometric shield with infinity symbol' Доступное имя SVG