- const points = Array(361).fill(null).map(() => '50,50').join(' ')

svg(xmlns="http://www.w3.org/2000/svg", viewBox="0 0 100 100")
  polyline#inner(points=points, fill="none")
  polyline#outer(points=points, fill="none")
View Compiled
:root,
body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  background-color: var(--color-background);
  color: var(--color-line);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 4px;
  margin: 0 auto;
  font-family: sans-serif;
}

svg {
  max-width: 90vmin;
  overflow: visible;
  shape-rendering: geometricprecision;
  stroke-linejoin: round;
}

#outer {
  stroke-width: var(--stroke-width-border, 0.4);
  stroke: var(--color-border);
}

#inner {
  stroke-width: var(--stroke-width-line, 0.2);
  stroke: var(--color-line);
}
View Compiled
const duration = 0.25;
const ease = "power2";

gsap.registerPlugin(MorphSVGPlugin);

const svg = document.querySelector("svg");

let n = 2;
let d = 29;

const knobSettings = {
  knobsToggle: false,
  visible: 0,
  CSSVarTarget: document.documentElement,
  knobs: [
    "Rose Parameters",
    {
      label: "n",
      type: "number",
      value: n,
      min: 2,
      step: 1,
      onChange: (e) => (n = Number(e.target.value)) && morph()
    },
    {
      label: "d",
      type: "number",
      value: d,
      min: 1,
      step: 0.1,
      onChange: (e) => (d = Number(e.target.value)) && morph()
    },
    "Styles",
    {
      label: "Border",
      type: "checkbox",
      checked: true,
      onChange: (e) => {
        let opacity = e.target.checked ? 1 : 0;
        gsap.to("#outer", { opacity, duration, ease });
      }
    },
    {
      cssVar: ["stroke-width-border"], // alias for the CSS variable
      label: "Border Stroke Width",
      type: "number",
      min: 0,
      max: 10,
      step: 0.01,
      value: 0.4
    },
    {
      cssVar: ["stroke-width-line"], // alias for the CSS variable
      label: "Line Stroke Width",
      type: "number",
      min: 0,
      max: 10,
      step: 0.01,
      value: 0.2
    },
    {
      cssVar: ["color-background"], // alias for the CSS variable
      label: "Background Color",
      type: "color",
      value: "#E84855"
    },
    {
      cssVar: ["color-border"], // alias for the CSS variable
      label: "Border Color",
      type: "color",
      value: "#2D3047"
    },
    {
      cssVar: ["color-line"], // alias for the CSS variable
      label: "Line Color",
      type: "color",
      value: "#2D3047"
    }
  ]
};

const knobs = new Knobs(knobSettings);

function degToRad(deg) {
  return deg * (Math.PI / 180);
}

function generatePoint(i, factor) {
  let k = i * factor;
  let r = 50 * Math.sin(degToRad(k * n));
  let x = Math.sin(degToRad(k)) * r;
  let y = Math.cos(degToRad(k)) * r;

  return [x + 50, 50 - y];
}

function generatePoints() {
  const inner = [];
  const outer = [];

  for (let i = 0; i < 361; i++) {
    inner.push(generatePoint(i, d));
    outer.push(generatePoint(i, 1));
  }

  return { inner, outer };
}

function formatPoints(points = []) {
  return points.map((point) => point.join(",")).join(" ");
}

function morph() {
  const { inner, outer } = generatePoints();
  gsap.to("#inner", { morphSVG: formatPoints(inner), duration, ease });
  gsap.to("#outer", { morphSVG: formatPoints(outer), duration, ease });
}

morph();

setTimeout(knobs.toggle.bind(knobs), 500);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/@yaireo/knobs@latest
  2. https://unpkg.co/gsap@3/dist/gsap.min.js
  3. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/MorphSVGPlugin3.min.js