Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rendering user-input text in concentric circles

I've a "circle of gratitude" section on a page.

The desired design is below: Text shown arranged in concentric circles

The idea is the page will have an input box where user can input their name and, when they press the submit button, the name will be in the box on the right side.

I've tried with this:

const canvas = document.getElementById('gratitude-circle');
const ctx = canvas.getContext('2d');
const form = document.getElementById('gratitude-form');
const input = document.getElementById('name-input');

const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const circleRadius = 250;
const minTextGap = 10; // Minimum gap between texts
const names = [];

function clearCircle() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // Draw decorative wreath (stylized circle with leaves)
  ctx.save();
  ctx.beginPath();
  ctx.arc(centerX, centerY, circleRadius, 0, 2 * Math.PI);
  ctx.strokeStyle = '#7bbf8e';
  ctx.lineWidth = 4;
  ctx.shadowColor = '#b8e2c8';
  ctx.shadowBlur = 8;
  ctx.stroke();
  ctx.restore();
  // Draw simple leaf accents (simulate wreath)
  for (let i = 0; i < 24; i++) {
    const angle = (i / 24) * 2 * Math.PI;
    const leafX = centerX + (circleRadius - 18) * Math.cos(angle);
    const leafY = centerY + (circleRadius - 18) * Math.sin(angle);
    ctx.save();
    ctx.translate(leafX, leafY);
    ctx.rotate(angle + Math.PI / 2);
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.quadraticCurveTo(6, 8, 0, 18);
    ctx.quadraticCurveTo(-6, 8, 0, 0);
    ctx.fillStyle = '#b8e2c8';
    ctx.globalAlpha = 0.7;
    ctx.fill();
    ctx.globalAlpha = 1;
    ctx.restore();
  }
  // Draw central message
  ctx.save();
  ctx.font = '28px Segoe UI, Arial';
  ctx.fillStyle = '#7bbf8e';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText('Thank you', centerX, centerY - 28);
  ctx.font = 'bold 32px Segoe UI, Arial';
  ctx.fillStyle = '#4a8c6b';
  ctx.fillText('for being part', centerX, centerY + 4);
  ctx.font = '28px Segoe UI, Arial';
  ctx.fillStyle = '#7bbf8e';
  ctx.fillText('of our story.', centerX, centerY + 36);
  ctx.restore();
}

function drawNames() {
  clearCircle();
  if (names.length === 0) return;
  // Place names along the circumference
  const totalNames = names.length;
  const fontSize = 22;
  ctx.font = 'bold 22px Segoe UI, Arial';
  let totalArcLength = 0;
  let nameArcLengths = [];
  // First, measure arc length for each name
  for (let i = 0; i < totalNames; i++) {
    const name = names[i];
    const textWidth = ctx.measureText(name).width;
    // Arc length = width / radius
    const arc = textWidth / circleRadius;
    nameArcLengths.push(arc);
    totalArcLength += arc;
  }
  // Calculate starting angle so names are centered
  let gap = minTextGap / circleRadius;
  let totalGap = gap * totalNames;
  let startAngle = -totalArcLength / 2 - totalGap / 2;
  let angle = startAngle;
  for (let i = 0; i < totalNames; i++) {
    const name = names[i];
    const arc = nameArcLengths[i];
    const theta = angle + arc / 2;
    const x = centerX + circleRadius * Math.cos(theta);
    const y = centerY + circleRadius * Math.sin(theta);
    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(theta + Math.PI / 2);
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = '#333';
    ctx.fillText(name, 0, 0);
    ctx.restore();
    angle += arc + gap;
  }
}

form.addEventListener('submit', function(e) {
  e.preventDefault();
  const name = input.value.trim();
  if (name && names.length < 100) { // Limit to avoid overfilling
    names.push(name);
    drawNames();
    input.value = '';
  }
});

clearCircle();
body {
  background: #fff7f3;
  font-family: 'Segoe UI', Arial, sans-serif;
  margin: 0;
  padding: 0;
}

.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}

#gratitude-form {
  margin-bottom: 32px;
  display: flex;
  gap: 12px;
}

#name-input {
  padding: 10px 16px;
  border-radius: 20px;
  border: 1px solid #b2b2b2;
  font-size: 1.1rem;
  outline: none;
}

#gratitude-form button {
  padding: 10px 20px;
  border-radius: 20px;
  border: none;
  background: #7bbf8e;
  color: #fff;
  font-size: 1.1rem;
  cursor: pointer;
  transition: background 0.2s;
}

#gratitude-form button:hover {
  background: #5fa06e;
}

.circle-area {
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
}

#gratitude-circle {
  background: transparent;
  display: block;
  border-radius: 50%;
  box-shadow: 0 0 0 2px #e0e0e0;
}
<div class="container">
  <form id="gratitude-form">
    <input type="text" id="name-input" placeholder="Enter your name" autocomplete="off" required />
    <button type="submit">Add Name</button>
  </form>
  <div class="circle-area">
    <canvas id="gratitude-circle" width="600" height="600"></canvas>
  </div>
</div>

But the result looks like this: Text overlapping along the circumference of the circle

Is there any way to develop a dynamic area like this?

like image 245
saif Avatar asked May 23 '26 06:05

saif


1 Answers

Update

Example I are concentric circles, Example II is a spiral. The former can detect when a circle (ring) runs out of room it'll go to the next circle upwards. It's not perfect.

SVG

SVG Elements Needed

Element Comment
<svg> Of course
<g> This groups SVG elements (used in Example I only)
<path> This is the spiral or circle shape
<text> A wrapper for...
<textPath> This will mimic a <path> its synced to
<tspan> This wraps the text - text styles should be assigned here



Concentric Circles (Example I) ⭐

This is the Actual Solution

The requirements for each circle (aka ring) are as follows:

  • <path> for the circle shape. <circle> is too simple unfortunately.
  • Two <text>s, two <textPath>s, and two <tspan>s. One set of elements is hidden and used to test if a name can fit on the current circle. Two SVG properties are used to compare the size (eg circumference) of the current <path> and the length of the text in the corresponding <tspan>:
SVG Property Description
getTotalLength() returns the computed value for the total length of a <path>.
getComputedTextLength() returns the computed value for the length of text in a SVG element.

If a name is too big then it proceeds to the next circle. The font-size of each <tspan> increases for each circle as well. Aspects such as <svg> dimensions, viewBox dimensions, radius and xy position of the <path>s, the number of circles etc. are kept in an object (cfg). As an option, the defaultsΒ² of cfg (and fSize global variable) can be changed. If cfg default settings are used (recommended), there'll be 18 circles which can contain 62 names of maximum length (20 characters).

β‚‚see comments in Example I JS for details

Example I

Concentric Circles ⌾

Instructions

  1. Click anywhere within the current browser window.
  2. Enter a name.
  3. Click "Ok" button to add the name or...
  4. click "Cancel" button to exit and not add the name.
  5. Each name is also suffixed with " ❧ " as a delimiter.
  6. When all of the circles are full, the user will be notified via a popover.

Note

All of the names and the current active circle (ring) are autosaved to localStorage. Unfortunately, StackOverflow prohibits our browsers from using localStorage so the code for autosaving is commented out. For a fully functional example that autosaves, review this CodePen.

View the example in Full page mode.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title></title>
  <meta name="description" content="155 chars">

  <link href="https://fonts.googleapis.com" rel="preconnect">
  <link href="https://fonts.gstatic.com" rel="preconnect" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono&display=swap" rel="stylesheet">
  <style>
    :root {
      --cast:
        rgba(0, 0, 0, 0.30) 0 1.1875em 2.375em,
        rgba(0, 0, 0, 0.22) 0 0.9375em 0.75em;
      --outline:
        rgba(60, 64, 67, 0.3) 0 0.0625em 0.125em 0,
        rgba(60, 64, 67, 0.15) 0 0.125em 0.375em 0.125em;
      --inner:
        rgb(204, 219, 232) 0.1875em 0.1875em 0.375em 0 inset,
        rgba(255, 255, 255, 0.5) -0.1875em -0.1875em 0.375em 0.0625em inset;
      font-family: "Atkinson Hyperlegible Mono", sans-serif;
      font-optical-sizing: auto;
    }

    html,
    body {
      width: 100%;
      height: 100%;
      margin: 0;
    }

    main {
      display: grid;
      place-items: center;
      min-height: 100vh;
      margin: auto;
    }

    #box {
      width: fit-content;
    }

    #prompt {
      padding: 0;
      border: 0;
      border-radius: 8px;
      background: transparent;
      overflow: hidden;
      opacity: 0;
      transform: scaleY(0);
      transition: 0.7s allow-discrete;
      box-shadow: var(--cast);

      & form {
        padding-top: 0.5rem;
        border-radius: 8px;
        background: transparent;
      }

      & fieldset {
        padding: 0.75rem 1.25rem 1rem;
        border: 0;
        background: #DDD;
      }

      & legend {
        margin: 0 0 -0.75rem -0.5rem;
        font-variant: small-caps;
        font-size: 1.2rem;
        user-select: none;
      }

      & label {
        letter-spacing: 1px;
        user-select: none;
      }

      & input {
        margin-top: 0.25rem;
        padding: 0.125rem 0 0.125rem 0.5rem;
        border: 0.5px inset rgb(240 240 240);
        border-radius: 4px;
        outline: 0;
        font: inherit;
        box-shadow: var(--inner);
      }

      & footer {
        display: flex;
        justify-content: flex-end;
        align-items: center;
        gap: 0.25rem;
        padding: 1rem 0 0;
      }

      & button {
        width: 4.5rem;
        padding: 0.25rem;
        border: 0.5px outset rgb(240 240 240);
        border-radius: 4px;
        font: inherit;
        cursor: pointer;
        box-shadow: var(--outline);
      }
    }

    #prompt[open] {
      background: #DDD;
      opacity: 1;
      transform: scaleY(1);
    }

    @starting-style {
      #prompt[open] {
        opacity: 0;
        transform: scaleY(0);
      }
    }

    #prompt::backdrop {
      background-color: transparent;
      transition: 0.7s allow-discrete;
    }

    #prompt[open]::backdrop {
      background-color: rgb(0 0 0 / 35%);
    }

    @starting-style {
      #prompt[open]::backdrop {
        background-color: transparent;
      }
    }

    #msg {
      border: 0.5px rgb(128 128 128) solid;
      font-size: 1.15rem;
      background: transparent;
      opacity: 0;
      transform: scaleY(0);
      transition: 0.7s allow-discrete;
      box-shadow: var(--cast);
    }

    #msg:popover-open {
      background: #FFF;
      opacity: 1;
      transform: scaleY(1);
    }

    @starting-style {
      #msg:popover-open {
        opacity: 0;
        transform: scaleY(0);
      }
    }

    #msg::backdrop {
      background-color: transparent;
      transition: 0.7s allow-discrete;
    }

    #msg:popover-open::backdrop {
      background-color: rgb(0 0 0 / 35%);
    }

    @starting-style {
      #msg:popover-open::backdrop {
        background-color: transparent;
      }
    }

    .info {
      display: inline-block;
      font-style: normal;
      color: rgb(43, 123, 237);
    }

    tspan {
      font-variant: small-caps;
      line-height: 1.2;
    }
  </style>
</head>

<body>

  <dialog id="prompt">
    <form id="ui" method="dialog">
      <fieldset>
        <legend>Nesting Circles</legend>
        <label>Enter a Name:
          <input id="add" maxlength="20"> 
          <i class="info" title="Characters (including spaces) are limited to 20 for each name.">&#9432;</i>
        </label>
        <footer>
          <button type="reset">Cancel</button>
          <button>Ok</button>
        </footer>
      </fieldset>
    </form>
  </dialog>

  <main>
    <object id="box" form="ui"></object>
  </main>

  <object id="msg" form="ui" popover></object>
  <script>
    let SVG, TEXT, TEST, paths, texts, tests, tspans, hide;
    let data = [];
    let state = 0;
    let done = false;

    /*π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜
    data = JSON.parse(localStorage.getItem("names")) || [];
    state = parseInt(localStorage.getItem("ring"), 10) || 0;
    done =  Boolean(localStorage.getItem("done")) || false;
    
    const saveData = (data) => localStorage.setItem("names", JSON.stringify(data));
    const saveState = (state, done) => {
      localStorage.setItem("ring", JSON.stringify(state)); 
      let int = +done;
      localStorage.setItem("done", JSON.stringify(int));
    };

    const setData = (data) => tspans.forEach((ts, i) => ts.textContent = data[i]); 
    π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜*/

    let last = "";
    let ring = state;
    let over = false;

    const prt = document.getElementById("prompt");
    const ui = document.forms.ui;
    const io = ui.elements;

    const NS = "http://www.w3.org/2000/svg";

    /**𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖
     * The following values can be adjusted.
     * @param {number} fSize - The base font-size in px.
     * @param {object} cfg - The configuration settings.
     *   @var {number} cfg.vw   - viewBox width of the <svg> in px.
     *   @var {number} cfg.vh   - viewBox height of the <svg> in px.
     *   @var {number} cfg.r    - The increment radius of the <path>s in px.
     *   @var {number} cfg.font - The increment font-size of the <tspan>s in px.
     *   @var {number} cfg.qty  - The number of circles (<path>s) to generate.
     */
    const fSize = 18;
    const cfg = {
      vw: 1800,
      vh: 1800,
      r: 40,
      font: 3,
      qty: 18
    };
    /*𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖𝄖*/
    cfg.cx = cfg.vw / 2;
    cfg.cy = cfg.vh / 2;

    const svgPathGenerator = function*(cfg) {
      let i = 0;
      let R = 0;
      let f = 0;
      while (i < cfg.qty) {
        f += (cfg.font * 0.25);
        R += cfg.r + f;
        let d = ` M ${cfg.cx} -${cfg.cy}
      m ${R}, 0
      a ${R},${R} 0 1,0 -${R * 2},0
      a ${R},${R} 0 1,0 ${R * 2},0
    `;
        const path = document.createElementNS(NS, "path");
        path.id = "path" + i;
        path.setAttribute("d", d);
        path.setAttribute("fill", "transparent");
        path.setAttribute("stroke", "#000");
        path.setAttribute("stroke-width", "1");
        path.setAttribute("transform", "scale(1,-1)");
        i++;
        yield path;
      }
    };
    const svgTextGenerator = function*(cfg, prefix) {
      let i = 0;
      while (i < cfg.qty) {
        const text = document.createElementNS(NS, "text");
        text.setAttribute("width", cfg.cx);
        const textPath = document.createElementNS(NS, "textPath");
        textPath.setAttribute("href", "#path" + i);
        const tspan = document.createElementNS(NS, "tspan");
        tspan.id = prefix + i;
        tspan.setAttribute("font-size", ((cfg.font * i) + fSize) + "px");
        text.appendChild(textPath).appendChild(tspan);
        i++;
        yield text;
      }
    };

    const init = (cfg) => {
      const base = `<svg id="SVG" xmlns="http://www.w3.org/2000/svg" width="${cfg.cx}" height="${cfg.cy}" viewBox="0 0 ${cfg.vw} ${cfg.vh}">
          <g id="PATH"></g>
          <g id="TEXT"></g>
          <g id="TEST" x="${cfg.vw}" y="${cfg.vh}" style="visibility: hidden"></g>
        </svg>`;
      io.box.insertAdjacentHTML("beforeend", base);
      SVG = document.getElementById("SVG");
      TEXT = document.getElementById("TEXT");
      TEST = document.getElementById("TEST");
      paths = [...svgPathGenerator(cfg)].map(p => document.getElementById("PATH").appendChild(p));
      texts = [...svgTextGenerator(cfg, "text")].map(t => TEXT.appendChild(t));
      tests = [...svgTextGenerator(cfg, "test")].map(t => TEST.appendChild(t));
      /*π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜
      tspans = Array.from(TEXT.querySelectorAll("tspan"));
      if (state > 0) {
        setData(data);
      }
      π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜*/
      if (!done) {
        prt.showModal();
        io.add.focus();
      }
    };

    const findPath = () => document.getElementById("path" + ring);
    const getText = () => document.getElementById("text" + ring);
    const getTest = () => document.getElementById("test" + ring);
    const checkLimit = () => {
      return getTest().getComputedTextLength() > findPath().getTotalLength();
    };
    const setText = () => {
      let text = getText();
      let test = getTest();
      if (io.add.value.trim().length < 1) return;
      const nameTag = io.add.value + " ❧ ";
      last = nameTag;
      test.insertAdjacentText("beforeend", nameTag);
      over = checkLimit();
      if (over) {
        ring++;
        if (ring === cfg.qty) {
          io.msg.textContent = "Circles are full.";
          io.msg.showPopover();
          io.box.onclick = null;
          done = true;
          return;
        }
        return setText();
      }
      text.insertAdjacentText("beforeend", last);
      over = checkLimit();
      /*π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜
      data = tspans.map(ts => ts.textContent || "");
      saveData(data);
      saveState(ring, done);
      π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜*/
    };

    io.box.onclick = (e) => prt.showModal();
    ui.onreset = (e) => prt.close();
    prt.onclose = (e) => {
      setText();
      io.add.value = "";
    };

    hide = () => setTimeout(() => io.msg.hidePopover(), 3500);
    const hint = (e) => {
      io.msg.textContent = "Click anywhere within this browser window to add a name.";
      io.msg.showPopover();
      hide();
    };
    prt.addEventListener("close", hint, {
      once: true
    });

    document.onkeydown = (e) => {
      if (e.code === "KeyC" && e.altKey) {
        /*π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜
        localStorage.clear();
        π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜π„˜*/
        location.reload();
        init(cfg);
      }
    };

    init(cfg);
  </script>
</body>

</html>


The Spiral (Example II)

Example I is the Actual Answer

This isn't what the OP wanted but I'm keeping it here for prosperity. Example I is the actual solution to OP.

  • The code that drew the SVG spiral is here.
  • The procedure of setting text along a curve is here.
  • Besides the research, my small contribution is configuring the <tspan> so that the text starts outside not at the center.

The markup for the <tspan>:

<tspan id="text" x="700%" text-anchor="end">

The [x] attribute determines where an SVG element is positioned horizontally (left/right). A value of 100% sets the <tspan> at the end of its parent element normally, but because its synced to the <path> (via <textPath x link:href="#spiral">), its true size is greater. I don't know if this is mathematically correct, but this is how I got [x="700%"]:

<svg viewBox="0 0 800 800"> <!-- 800px wide -->

  <path id="spiral"> <!-- path & textPath are synced -->

  <text width="800"> <!-- Should be as wide as the viewbox -->

    <textPath href="#spiral"> <!-- #id of path for sync -->

      <!-- - setting [x="100%"] to the tspan set it about an eighth 
             of the way from the center
           - setting [x="800%"] to the tspan set it past the end of 
             the spiral
           - setting [x="700%"] brought the beginning of the tspan 
             well within the limits of the spiral.
           - text-anchor="end" is like text-align: right -->  

      <tspan id="text" x="700%" text-anchor="end"></tspan>
    </textPath>
  </text>
</svg>

In the example,

  • <svg>, <path>, and functions getIntercept()ΒΉ, setPoint()ΒΉ, and getPath() makes path#spiral by calculating the value of its [d] attribute. (see post)

  • <text> and <textPath> syncs the text to the <path> (see article)

  • <tspan>, the rest of the markup, and the event handlers allows the user to add their name to the spiral. (see below).

₁function name was changed

Example II

Spiral πŸ˜΅β€πŸ’«

Instructions

  1. Click the spiral.
  2. Enter a name.
  3. Click "Ok" button to add the name or...
  4. click "Cancel" button to exit and not add the name.
  5. Each name is also suffixed with " ❧ " as a delimiter.

View the example in Full page mode.

const modal = document.getElementById("modal");

const spiral = document.getElementById("spiral");
const text = document.getElementById("text");

const ui = document.forms.ui;
const io = ui.elements;

const getInterept = (x1, y1, x2, y2) => {
  if (x1 === x2) return;
  const x = (y2 - y1) / (x1 - x2);
  return {
    x: x,
    y: x1 * x + y1
  };
};

const setPoint = (point) => {
  return `${point.x},${point.y} `;
};

const getPath = (center, startRadius, spacePerLoop, startTheta, endTheta, thetaStep) => {
  const a = startRadius;
  const b = spacePerLoop / Math.PI / 2;
  let newTheta = startTheta * Math.PI / 180;
  let oldTheta = newTheta;
  endTheta = endTheta * Math.PI / 180;
  thetaStep = thetaStep * Math.PI / 180;

  let oldR,
    newR = a + b * newTheta;

  const oldPoint = {
    x: 0,
    y: 0
  };
  const newPoint = {
    x: center.x + newR * Math.cos(newTheta),
    y: center.y + newR * Math.sin(newTheta)
  };

  let oldslope,
    newSlope =
    (b * Math.sin(oldTheta) + (a + b * newTheta) * Math.cos(oldTheta)) /

    (b * Math.cos(oldTheta) - (a + b * newTheta) * Math.sin(oldTheta));

  let path = "M " + setPoint(newPoint);

  while (oldTheta < endTheta - thetaStep) {
    oldTheta = newTheta;
    newTheta += thetaStep;

    oldR = newR;
    newR = a + b * newTheta;

    oldPoint.x = newPoint.x;
    oldPoint.y = newPoint.y;
    newPoint.x = center.x + newR * Math.cos(newTheta);
    newPoint.y = center.y + newR * Math.sin(newTheta);

    const aPlusBTheta = a + b * newTheta;

    oldSlope = newSlope;
    newSlope =
      (b * Math.sin(newTheta) + aPlusBTheta * Math.cos(newTheta)) /
      (b * Math.cos(newTheta) - aPlusBTheta * Math.sin(newTheta));

    const oldIntercept = -(oldSlope * oldR * Math.cos(oldTheta) - oldR * Math.sin(oldTheta));
    const newIntercept = -(newSlope * newR * Math.cos(newTheta) - newR * Math.sin(newTheta));

    const controlPoint = getInterept(oldSlope, oldIntercept, newSlope, newIntercept);

    controlPoint.x += center.x;
    controlPoint.y += center.y;
    path += "Q " + setPoint(controlPoint) + setPoint(newPoint);
  }
  return path;
};

const setText = () => {
  const nameTag = io.add.value;
  if (nameTag.trim().length < 1) return;
  text.insertAdjacentText("beforeend", nameTag + " ❧ ");
};

io.box.onclick = (e) => modal.showModal();

ui.onreset = (e) => {
  modal.close();
};

modal.onclose = (e) => {
  setText();
  io.add.value = "";
};

const path = getPath({
  x: 400,
  y: 400
}, 0, 50, 0, 6 * 360, 30);

spiral.setAttribute("d", path);
:root {
  font: 2ch/1.5 "Segoe UI";
}

html,
body {
  width: 100%;
  height: 100%;
  margin: o;
  overflow-x: hidden;
}

main {
  display: grid;
  place-items: center;
  min-height: 100vh;
  margin: auto;
}

dialog {
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  overflow: hidden;
  opacity: 0;
  transform: scaleY(0);
  transition: 0.7s allow-discrete;
  box-shadow:
    rgb(38, 57, 77) 0 1.25rem 1.875rem -0.625rem;

  & form {
    padding-top: 0.5rem;
    border-radius: 8px;
    background: transparent;
  }

  & fieldset {
    padding: 0.75rem 1.25rem 1rem;
    border: 0;
    background: #DDD;
  }

  & legend {
    margin: 0 0 -0.75rem -0.5rem;
    font-variant: small-caps;
    font-size: 1.2rem;
    user-select: none;
  }

  & label {
    letter-spacing: 1px;
    user-select: none;
  }

  & input {
    margin-top: 0.25rem;
    padding: 0.125rem 0 0.125rem 0.5rem;
    border: 0.5px inset rgb(240 240 240);
    border-radius: 4px;
    outline: 0;
    font: inherit;
    box-shadow:
      rgb(204, 219, 232) 0.1875rem 0.1875rem 0.375rem 0 inset,
      rgba(255, 255, 255, 0.5) -0.1875rem -0.1875rem 0.375rem 0.0625rem inset;
  }

  & footer {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 0.25rem;
    padding: 1rem 0 0;
  }

  & button {
    width: 4rem;
    padding: 0.25rem;
    border: 0.5px outset rgb(240 240 240);
    border-radius: 4px;
    font: inherit;
    cursor: pointer;
    box-shadow:
      box-shadow: rgba(60, 64, 67, 0.3) 0 0.0625rem 0.125rem 0,
      rgba(60, 64, 67, 0.15) 0 0.125rem 0.375rem 0.125rem;
  }
}

dialog[open] {
  background: #DDD;
  opacity: 1;
  transform: scaleY(1);
}

@starting-style {
  dialog[open] {
    opacity: 0;
    transform: scaleY(0);
  }
}

dialog::backdrop {
  background-color: transparent;
  transition: 0.7s allow-discrete;
}

dialog[open]::backdrop {
  background-color: rgb(0 0 0 / 35%);
}

@starting-style {
  dialog[open]::backdrop {
    background-color: transparent;
  }
}

#text {
  font-variant: small-caps;
  font-size: 2.5rem;
  line-height: 1;
}
<dialog id="modal">
  <form id="ui" method="dialog">
    <fieldset>
      <legend>Downward Uzumaki</legend>
      <label>Enter a Name: <br>
        <input id="add">
      </label>
      <footer>
        <button>Ok</button>
        <button type="reset">Cancel</button>
      </footer>
    </fieldset>
  </form>
</dialog>

<main>
  <object id="box" form="ui">
    <svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 800 800">
      <path id="spiral" d="" fill="none" stroke="black" stroke-width="3"/>
      <text width="800">
        <textPath xlink:href="#spiral">
          <tspan id="text" x="700%" text-anchor="end"></tspan>
        </textPath>
      </text>
    </svg>
  </object>
</main>
like image 193
zer00ne Avatar answered May 24 '26 20:05

zer00ne



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!