<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="author" content="@ajotanc" />

    <title>Pixel Magic Tool</title>

    <style>
      :root {
        --s-thumb: #0006;
        --s-background: #0003;
        --overlay: rgba(0, 0, 0, 0.5);
        --background: #111;
        --text: #bbb;
        --gray-dark: #222;
        --gray-medium: #333;
        --gray-light: #eeeeee;
        --blue-dark: #284352;
        --blue-medium: #3b708b;
        --blue-light: #48a;
        --success-dark: #03440c;
        --success-medium: #548c2f;
        --success-light: #8ac926;
        --error-dark: #a80a0a;
        --error-medium: #c9262b;
        --error-light: #ff595e;
        --warning-dark: #dc2f02;
        --warning-medium: #e85d04;
        --warning-light: #f48c06;
      }

      ::-webkit-scrollbar {
        width: 6px;
      }

      ::-webkit-scrollbar-track {
        background: transparent;
      }

      ::-webkit-scrollbar-thumb {
        background: var(--s-thumb);
        opacity: 0.2;
        border-radius: 5px;
      }

      ::-webkit-scrollbar-thumb:hover {
        background: var(--s-background);
      }

      ::selection {
        background: var(--blue-light);
      }

      * {
        font-family: "Helvetica", Verdana, sans-serif;
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      body {
        margin: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        background: var(--background);
      }

      canvas {
        width: 100%;
      }

      small {
        display: block;
        font-weight: 400;
        margin: 2px 0 5px;
        color: var(--gray-light);
        font-size: 0.75rem;
      }

      footer {
        display: flex;
        justify-content: center;
        margin-block: 20px;
      }

      a {
        text-decoration: none;
        color: var(--blue-light);
        font-size: 0.75rem;
        font-weight: 600;
      }

      a:is(:hover, :focus, :active) {
        color: var(--blue-medium);
      }

      #wledEdit {
        padding: 1.5px 8px;
        background: var(--blue-light);
        margin-left: 6px;
        border-radius: 4px;
        color: var(--gray-light);
      }

      .container {
        width: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
      }

      .content {
        width: min(768px, calc(100% - 40px));
        margin-inline: 20px;
      }

      .row {
        display: flex;
        flex-wrap: nowrap;
        justify-content: space-between;
        margin-top: 20px;
      }

      .col {
        flex-basis: calc(50% - 10px);
        position: relative;
        padding-inline: 5px;
      }

      .col-full {
        flex-basis: 100%;
        position: relative;
        padding-inline: 5px;
      }

      .col-small {
        flex-basis: 33.3333%;
        position: relative;
        padding-inline: 5px;
      }

      .header {
        display: flex;
        flex-direction: column;
        padding-block: 20px;
      }

      .header .title {
        font-size: 2.5rem;
        line-height: 2.5rem;
        font-weight: 800;
        color: var(--gray-light);
        padding-bottom: 5px;
      }

      .header .subtitle {
        font-size: 0.75rem;
        color: var(--text);
      }

      .header .rainbow {
        background: linear-gradient(
          to right,
          #ef5350,
          #f48fb1,
          #7e57c2,
          #2196f3,
          #26c6da,
          #43a047,
          #eeff41,
          #f9a825,
          #ff5722
        );
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-size: 200% 200%;
        animation: rainbow 5s linear infinite;
        transition: background-position 0.5s ease;
      }

      label {
        display: flex;
        margin-bottom: 5px;
        font-weight: bold;
        color: var(--text);
        align-items: center;
      }

      input[type="text"],
      input[type="number"],
      select,
      textarea {
        width: 100%;
        padding: 10px;
        border-radius: 50px;
        background-color: var(--gray-medium);
        border: 1px solid var(--gray-medium);
        outline: none;
        color: var(--gray-light);
        font-size: 0.875rem;
      }

      .input-group {
        display: flex;
        justify-content: center;
        align-items: center;
      }

      .input-group input:not([type="range"]) {
        border-radius: 50px 0 0 50px;
      }

      .input-group .input-description {
        width: 100%;
        max-width: 38px;
        height: 38px;
        display: flex;
        justify-content: center;
        align-items: center;
        color: var(--gray-dark);
        background: var(--gray-light);
        border-radius: 0px 50px 50px 0;
        border: 1px solid var(--gray-light);
        border-left: 0;
        font-size: 0.875rem;
        line-height: 1rem;
      }

      .input-group .square {
        border-radius: 50px !important;
        margin-left: 10px;
      }

      .input-group .square input {
        text-align: center;
        background: none;
        padding: 0;
        border: 0;
        color: var(--gray-dark);
      }

      textarea {
        resize: none;
        border-radius: 8px;
      }

      .custom-select select {
        appearance: none;
        -webkit-appearance: none;
        -moz-appearance: none;
        background-image: none;
        padding-right: 39px;
        cursor: pointer;
      }

      .custom-select label::after {
        content: "";
        position: absolute;
        top: calc(50% + 6px);
        right: 21px;
        transform: rotate(135deg);
        width: 6px;
        height: 6px;
        border-top: 2px solid var(--gray-light);
        border-right: 2px solid var(--gray-light);
        pointer-events: none;
      }

      .dropzone {
        width: 100%;
        border: 1px dashed var(--gray-light);
        background-color: var(--gray-dark);
        color: var(--gray-light);
        text-align: center;
        padding: 40px 10px;
        border-radius: 8px;
        margin-top: 20px;
        transition: all 0.5s ease-in-out;
      }

      .dropzone:hover {
        cursor: pointer;
        color: var(--gray-dark);
        background-color: var(--gray-light);
        border-color: var(--gray-dark);
      }

      .dropzone.dragover {
        background-color: var(--gray-medium);
      }

      .range-slider {
        appearance: none;
        background-color: var(--gray-light);
        height: 8px;
        width: 100%;
        border-radius: 10px;
        outline: none;
        margin-block: 15px;
      }

      .range-slider::-webkit-slider-thumb,
      .range-slider::-moz-range-thumb {
        appearance: none;
        height: 16px;
        width: 16px;
        background-color: var(--blue-light);
        border-radius: 50%;
        cursor: pointer;
        border: 0;
      }

      .switch {
        position: relative;
        display: inline-block;
        width: 38px;
        height: 20px;
      }

      .switch input {
        outline: none;
        display: none;
      }

      .switch-slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: var(--gray-medium);
        border-radius: 34px;
        transition: 0.4s;
      }

      .switch-slider:before {
        position: absolute;
        content: "";
        height: 14px;
        width: 14px;
        left: 3px;
        bottom: 3px;
        background-color: white;
        border-radius: 50%;
        transition: 0.4s;
      }

      input:checked + .switch-slider {
        background-color: var(--blue-light);
      }

      input:focus + .switch-slider {
        box-shadow: 0 0 1px var(--blue-light);
      }

      input:checked + .switch-slider:before {
        transform: translateX(18px);
      }

      #toast-container {
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 9999;
      }

      .toast {
        display: flex;
        align-items: center;
        width: auto;
        padding: 6px 12px;
        margin-top: 10px;
        border-radius: 8px;
        transform: translateY(30px);
        opacity: 0;
        visibility: hidden;
      }

      .toast .toast-body {
        padding-block: 8px;
        font-weight: 600;
        color: var(--text);
        letter-spacing: 0.5px;
      }

      .toast.success {
        background-color: var(--success-medium);
      }

      .toast.error {
        background-color: var(--error-light);
      }

      .toast.warning {
        background-color: var(--warning-light);
      }

      .toast-progress {
        position: absolute;
        left: 4px;
        bottom: 4px;
        width: calc(100% - 8px);
        height: 3px;
        transform: scaleX(0);
        transform-origin: left;
        border-radius: 8px;
      }

      .toast.success .toast-progress {
        background: linear-gradient(
          to right,
          var(--success-light),
          var(--success-medium)
        );
      }

      .toast.error .toast-progress {
        background: linear-gradient(
          to right,
          var(--error-light),
          var(--error-medium)
        );
      }

      .toast.warning .toast-progress {
        background: linear-gradient(
          to right,
          var(--warning-light),
          var(--warning-medium)
        );
      }

      .carousel {
        display: flex;
        height: 100%;
        width: 100%;
        cursor: pointer;
      }

      .button {
        width: 100%;
        border: 0;
        padding: 10px 18px;
        border-radius: 50px;
        color: var(--text);
        cursor: pointer;
        margin-bottom: 10px;
        background: var(--gray-medium);
        border: 1px solid var(--gray-dark);
        transition: all 0.5s ease-in-out;
        font-size: 0.875rem;
        font-weight: 600;
      }

      .button:hover {
        background: var(--gray-dark);
        border: 1px solid var(--gray-medium);
      }

      .button:last-child {
        margin-bottom: 0;
      }

      .buttons {
        display: flex;
        flex-direction: column;
        width: 100%;
      }

      #overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: var(--overlay);
        display: none;
      }

      #overlay.loading::after {
        content: "";
        display: block;
        position: absolute;
        top: calc(50% - 19px);
        left: calc(50% - 19px);
        width: 26px;
        height: 26px;
        border: 6px solid var(--gray-light);
        border-top-color: var(--gray-dark);
        border-radius: 50%;
        animation: spin 1s linear infinite;
      }

      #recreatedImage {
        margin-block: 20px;
      }

      .invalid {
        border: 1px solid var(--error-dark) !important;
      }

      .error-message {
        display: block;
        color: var(--error-dark);
        padding-block: 4px;
        font-weight: 600;
        font-size: 0.75rem;
      }

      @media (max-width: 767px) {
        .row {
          flex-wrap: wrap;
          flex-direction: column;
          margin: 0;
        }

        .col,
        .col-full,
        .col-small {
          flex-basis: 100%;
          margin-top: 20px;
          padding: 0;
        }
      }

      @keyframes spin {
        to {
          transform: rotate(360deg);
        }
      }

      @keyframes progress {
        to {
          transform: scaleX(1);
        }
      }

      @keyframes fadeIn {
        5% {
          opacity: 1;
          visibility: visible;
          transform: translateY(0);
        }

        95% {
          opacity: 1;
          transform: translateY(0);
        }
      }

      @keyframes rainbow {
        0% {
          background-position: 0% 0;
        }
        100% {
          background-position: 200% 0;
        }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="content">
        <form id="formGenerate" novalidate>
          <div class="header">
            <span class="title"
              >PIXEL <span class="rainbow">MAGIC</span> TOOL</span
            >
            <span class="subtitle"
              >It is a tool that converts any image into code in
              <strong>JSON WLED</strong> format for
              <strong>2D Matrix</strong> panels</span
            >
          </div>
          <div class="row">
            <div class="col" validate>
              <label for="hostname">Hostname</label>
              <input type="text" name="hostname" id="hostname" required />
            </div>
            <div class="col" validate>
              <label for="name">Preset Name</label>
              <input
                type="text"
                name="name"
                id="name"
                value="New Preset"
                required />
            </div>
          </div>
          <div class="row">
            <div class="col" validate>
              <div class="custom-select">
                <label for="pattern">Pattern</label>
                <select name="pattern" id="pattern" required>
                  <option value="1" title="['ffffff']">Individual</option>
                  <option value="2" title="[0, 'ffffff']">Index</option>
                  <option value="3" title="[0, 5, 'ffffff']" selected>
                    Range
                  </option>
                </select>
              </div>
            </div>
            <div class="col" validate>
              <div class="custom-select">
                <label for="output">Output</label>
                <select name="output" id="output" required>
                  <option value="json" selected>WLED JSON</option>
                  <option value="ha">Home Assistant</option>
                  <option value="curl">CURL</option>
                </select>
              </div>
            </div>
          </div>
          <div class="row output" style="display: none">
            <div class="col" validate>
              <label for="device">Device</label>
              <input type="text" name="device" id="device" required />
            </div>
            <div class="col" validate>
              <label for="uniqueId">Unique Id</label>
              <input type="text" name="uniqueId" id="uniqueId" required />
            </div>
            <div class="col" validate>
              <label for="friendlyName">Friendly Name</label>
              <input
                type="text"
                name="friendlyName"
                id="friendlyName"
                required />
            </div>
          </div>
          <div class="row">
            <div class="col" validate>
              <div class="custom-select">
                <label for="segments">Segment Id</label>
                <select name="segments" id="segments">
                  <option value="0" data-width="16" data-height="16">
                    Segment 0
                  </option>
                </select>
              </div>
            </div>
            <div class="col" validate>
              <label for="brightness">Brightness</label>
              <div class="input-group">
                <input
                  type="range"
                  name="brightness"
                  id="brightness"
                  min="0"
                  max="255"
                  value="128"
                  class="range-slider" />
                <div class="input-description square">
                  <input
                    type="text"
                    name="brightnessValue"
                    id="brightnessValue"
                    value="128" />
                </div>
              </div>
            </div>
          </div>
          <div class="row">
            <div class="col" validate>
              <label for="animation">Animation</label>
              <label class="switch">
                <input
                  type="checkbox"
                  name="animation"
                  id="animation"
                  data-parent="animation" />
                <span class="switch-slider"></span>
              </label>
            </div>
            <div class="col" validate>
              <label for="transparentImage">Transparent Image</label>
              <label class="switch">
                <input
                  type="checkbox"
                  name="transparentImage"
                  id="transparentImage"
                  data-parent="transparentImage" />
                <span class="switch-slider"></span>
              </label>
            </div>
            <div class="col" validate>
              <label for="resizeImage">Resize Image</label>
              <label class="switch">
                <input
                  type="checkbox"
                  name="resizeImage"
                  id="resizeImage"
                  data-parent="resizeImage"
                  checked />
                <span class="switch-slider"></span>
              </label>
            </div>
          </div>
          <div class="row resizeImage">
            <div class="col" validate>
              <label for="width">Width</label>
              <input type="number" name="width" id="width" value="16" />
            </div>
            <div class="col" validate>
              <label for="height">Height</label>
              <input type="number" name="height" id="height" value="16" />
            </div>
          </div>
          <div class="row animation" style="display: none">
            <div class="col" validate>
              <label for="frames">Frames</label>
              <input type="number" name="frames" id="frames" value="4" />
            </div>
            <div class="col" validate>
              <label for="duration">Duration</label>
              <div class="input-group">
                <input
                  type="number"
                  name="duration"
                  id="duration"
                  value="0.5"
                  required="required"
                  min="0"
                  step="0.1"
                  inputmode="numeric" />
                <div class="input-description">
                  <span>sec</span>
                </div>
              </div>
            </div>
            <div class="col" validate>
              <label for="transition">Transition</label>
              <div class="input-group">
                <input
                  type="number"
                  name="transition"
                  id="transition"
                  value="0.2"
                  required="required"
                  min="0"
                  step="0.1"
                  inputmode="numeric" />
                <div class="input-description">
                  <span>sec</span>
                </div>
              </div>
            </div>
          </div>
          <div class="row transparentImage" style="display: none">
            <div class="col-full" validate>
              <label for="color">Choose a color</label>
              <small>
                Color that will replace the
                <strong>transparent pixels</strong> in the image
              </small>
              <input type="color" name="color" id="color" value="#00BFFF" />
            </div>
          </div>
          <div class="row">
            <div class="col-full" validate>
              <div class="custom-select">
                <label for="images">
                  <span>Images upload to WLED</span>
                  <a id="wledEdit" href="http://[wled-ip]/edit" target="_blank">
                    upload
                  </a>
                </label>
                <select name="images" id="images">
                  <option value="">Select image</option>
                </select>
              </div>
            </div>
          </div>
          <div id="dropzone" class="dropzone" validate>
            <p id="dropzoneLabel">
              Drag and drop a file here or click to select a local file
            </p>
            <input
              type="file"
              name="source"
              id="source"
              accept="image/*"
              style="display: none" />
          </div>
          <div class="row">
            <div class="col-full">
              <button type="button" class="button" id="btnGenerate">
                Generate
              </button>
            </div>
            <div class="col-small" id="gbth" style="display: none">
              <button
                type="button"
                class="button"
                onclick="window.location.href = WLED_URL;">
                Back
              </button>
            </div>
          </div>
        </form>
        <div id="preview" style="display: none">
          <div id="recreatedImage"></div>
          <textarea name="response" id="response" rows="8" readonly="readonly">
          </textarea>
          <div class="buttons">
            <div class="row">
              <div class="col">
                <button type="button" class="button" id="btnCopyToClipboard">
                  Copy to Clipboard
                </button>
              </div>
              <div class="col">
                <button type="button" class="button" id="btnSave">Save</button>
              </div>
              <div class="col">
                <button type="button" class="button" id="btnDownloadPreset">
                  Download
                </button>
              </div>
            </div>
            <div class="row" id="simulate" style="display: none">
              <div class="col-full">
                <button type="button" class="button" id="btnSimulatePreset">
                  Simulate
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
      <footer>
        <a href="https://github.com/ajotanc/PixelMagicTool" target="_blank">
          Github &copy; Pixel Magic Tool
        </a>
      </footer>
    </div>
    <div id="toast-container"></div>
    <div id="overlay"></div>
  </body>
  <script>
    const d = document;
    const params = new URLSearchParams(window.location.search);
    const host = params.get("hn")
      ? params.get("hn")
      : window.location.host
      ? window.location.host
      : "0.0.0.0";
    const protocol =
      window.location.protocol === "file:" ? "http:" : window.location.protocol;

    let WLED_URL = `${protocol}//${host}`;

    const hostname = gId("hostname");
    hostname.value = host;

    hostname.addEventListener("blur", async () => {
      WLED_URL = `${protocol}//${hostname.value}`;

      await segments();
      await images();

      hostnameLabel();
    });

    gId("gbth").style.display =
      window.location.protocol === "http:" ? "flex" : "none";

    let jsonSaveWLED = [];
    let jsonSendWLED = {};

    (async function () {
      await segments();
      await images();

      hostnameLabel();
    })();

    function gId(id) {
      return d.getElementById(id);
    }

    function hostnameLabel() {
      const link = gId("wledEdit");
      link.href = WLED_URL + "/edit";
    }

    async function playlist() {
      const { value: duration } = gId("duration");
      const { value: transition } = gId("transition");
      const { value: name } = gId("name");

      const urlPreset = `${WLED_URL}/presets.json`;
      const url = `${WLED_URL}/json`;

      try {
        const response = await fetch(urlPreset);
        const data = await response.json();
        const items = Object.keys(data);

        const ps = items.filter(
          (key) =>
            typeof data[key] === "object" &&
            !data[key].playlist &&
            data[key].n &&
            data[key].n.startsWith(name)
        );

        const id =
          items.find(
            (key) =>
              typeof data[key] === "object" &&
              data[key].playlist &&
              data[key].n &&
              data[key].n === name
          ) || parseInt(items.pop()) + 1;

        const body = {
          psave: parseInt(id),
          n: name,
          on: true,
          o: false,
          playlist: {
            ps,
            dur: Array.from({ length: ps.length }, () => duration * 10),
            transition: Array.from(
              { length: ps.length },
              () => transition * 10
            ),
            repeat: 0,
            end: 0,
          },
        };

        const options = {
          method: "POST",
          body: JSON.stringify(body),
        };

        try {
          const response = await fetch(url, options);
          const { success } = await response.json();

          if (success) {
            toast(`Playlist "${name}" save successfully`);
          }
        } catch (error) {
          toast(`Error saving preset: ${error}`, "error");
        }
      } catch (error) {
        toast(error, "error");
      }
    }

    async function insert(data, isAnimated = false, delay = 5000) {
      const urlPreset = `${WLED_URL}/presets.json`;
      const url = `${WLED_URL}/json`;

      let requestsCompleted = 0;

      show();

      const promises = data.map(async (item) => {
        return new Promise((resolve, reject) => {
          setTimeout(async () => {
            try {
              const response = await fetch(urlPreset);
              const data = await response.json();
              const items = Object.keys(data);

              const id =
                items.find(
                  (key) =>
                    typeof data[key] === "object" &&
                    !data[key].playlist &&
                    data[key].n === item.n
                ) || parseInt(items.pop()) + 1;

              const body = Object.assign(item, { psave: parseInt(id) });
              const options = {
                method: "POST",
                body: JSON.stringify(body),
              };

              try {
                const response = await fetch(url, options);
                const { success } = await response.json();

                if (success) {
                  toast(`Preset "${item.n}" save successfully`);
                  window.parent.postMessage("loadPresets", WLED_URL);
                }
              } catch (error) {
                toast(`Error saving preset: ${error}`, "error");
              }
            } catch (error) {
              toast(error, "error");
            } finally {
              resolve();
            }
          }, delay * requestsCompleted++);
        });
      });

      await Promise.all(promises);

      if (isAnimated) {
        setTimeout(async () => {
          await playlist()
            .then(() => {
              hide();
            })
            .finally(() => {
              hide();
            });
        }, delay);
      } else {
        hide();
      }
    }

    async function images() {
      const url = `${WLED_URL}/edit?list=/`;
      const select = gId("images");

      show();

      try {
        const response = await fetch(url);
        const data = await response.json();
        const mimeTypes = ["jpeg", "jpg", "png", "gif"];

        const images = data.filter((file) => {
          const { name } = file;
          const [filename, mimetype] = name.split(".");
          file.name = name.replace("/", "");
          return mimeTypes.includes(mimetype);
        });

        const options = [{ text: "Select image", value: "" }];

        if (images.length > 0) {
          options.push(
            ...images.map(({ name }) => ({
              text: name,
              value: `${WLED_URL}/${name}`,
            }))
          );

          select.innerHTML = "";

          options.forEach(({ text, value }) => {
            const option = new Option(text, value);

            if (index === 0) {
              option.selected = true;
            }

            select.appendChild(option);
          });
        }
      } catch (error) {
        toast(error, "error");
      } finally {
        hide();
      }
    }

    async function segments() {
      const select = gId("segments");
      const width = gId("width");
      const height = gId("height");

      show();

      try {
        const url = `${WLED_URL}/json/state`;
        const response = await fetch(url);
        const { seg: segments } = await response.json();

        const options = [
          { text: "Segment Default", value: 0, width: 16, height: 16 },
        ];

        if (segments) {
          options.splice(0);
          options.push(
            ...segments.map(({ id, n, start, stop, startY, stopY }) => ({
              text: n ? n : `Segment ${id}`,
              value: id,
              width: stop - start,
              height: stopY - startY,
            }))
          );
        }

        select.innerHTML = "";

        options.forEach(({ text, value, width: w, height: h }, index) => {
          const option = new Option(text, value);
          option.dataset.width = w;
          option.dataset.height = h;

          if (index === 0) {
            option.selected = true;
            width.value = w;
            height.value = h;
          }

          select.add(option);
        });
      } catch (error) {
        toast(error, "error");
      } finally {
        hide();
      }
    }

    gId("dropzone").addEventListener("dragover", (e) => {
      e.preventDefault();
    });

    gId("dropzone").addEventListener("drop", (e) => {
      e.preventDefault();

      source.files = e.dataTransfer.files;

      const { name } = source.files[0];
      gId("dropzoneLabel").textContent = `Image ${name} selected!`;

      validate(e);
    });

    gId("dropzone").addEventListener("click", () => {
      source.click();
    });

    gId("source").addEventListener("change", (e) => {
      const dropzoneLabel = gId("dropzoneLabel");
      const { value } = e.target;

      if (value) {
        const { name } = e.target.files[0];
        dropzoneLabel.textContent = `Image ${name} selected!`;
      } else {
        dropzoneLabel.textContent =
          "Drag and drop a file here or click to select a file";
      }

      validate(e);
    });

    gId("btnSimulatePreset").addEventListener("click", async () => {
      const url = `${WLED_URL}/json/state`;

      const options = {
        method: "POST",
        body: JSON.stringify(jsonSendWLED),
      };

      show();

      try {
        const response = await fetch(url, options);
        const { success } = await response.json();

        if (success) {
          toast("Successfully simulated preset");
        }
      } catch (error) {
        toast(error, "error");
      } finally {
        hide();
      }
    });

    gId("btnSave").addEventListener("click", async () => {
      const { checked } = gId("animation");
      const { value: name } = gId("name");

      if (checked) {
        await insert(jsonSaveWLED, true);
      } else {
        jsonSaveWLED.splice(0);

        jsonSaveWLED.push(
          Object.assign({}, jsonSendWLED, { n: name, o: false })
        );

        await insert(jsonSaveWLED);
      }
    });

    gId("btnCopyToClipboard").addEventListener("click", async () => {
      const response = gId("response");

      response.select();

      try {
        await navigator.clipboard.writeText(response.value);
        toast("Text copied to clipboard");
      } catch (error) {
        try {
          await d.execCommand("copy");
          toast("Text copied to clipboard");
        } catch (error) {
          toast(error, "error");
        }
      }
    });

    gId("btnDownloadPreset").addEventListener("click", () => {
      const { value: response } = gId("response");
      const { value: output } = gId("output");

      const timestamp = new Date().getTime();
      const filename = `WLED_${timestamp}`;

      downloadFile(response, filename, output);
    });

    gId("segments").addEventListener("change", (e) => {
      const { width, height } = e.target.selectedOptions[0].dataset;

      gId("width").value = w;
      gId("height").value = h;
    });

    gId("output").addEventListener("change", (e) => {
      const { value } = e.target.selectedOptions[0];

      d.querySelector(".output").style.display =
        value === "ha" ? "flex" : "none";
    });

    gId("brightnessValue").addEventListener("input", (e) => {
      const { value } = e.target;
      const bri = value <= 255 ? value : 255;

      gId("brightness").value = bri;
      e.target.value = bri;
    });

    gId("brightness").addEventListener("input", (e) => {
      const { value } = e.target;

      gId("brightnessValue").value = value;
    });

    gId("images").addEventListener("change", (e) => {
      const dropzone = gId("dropzone");
      const { value } = e.target.selectedOptions[0];

      if (!value) {
        const source = gId("source");

        gId("dropzoneLabel").textContent =
          "Drag and drop a file here or click to select a local file";
        source.value = "";

        dropzone.style.display = "block";
      } else {
        dropzone.style.display = "none";
      }
    });

    gId("transparentImage").addEventListener("change", (e) => {
      const { checked } = e.target;

      d.querySelector(".transparentImage").style.display = checked
        ? "flex"
        : "none";
    });

    gId("resizeImage").addEventListener("change", (e) => {
      const { checked } = e.target;

      d.querySelector(".resizeImage").style.display = checked ? "flex" : "none";
    });

    gId("animation").addEventListener("change", (e) => {
      const animation = d.querySelector(".animation");
      const source = gId("source");

      const { checked } = e.target;

      if (checked) {
        toast(
          'If you want all frames in the image, set it to "0"',
          "warning",
          5000
        );

        source.setAttribute("accept", "image/gif");
        animation.style.display = "flex";
      } else {
        source.setAttribute("accept", "image/*");
        animation.style.display = "none";
      }
    });

    gId("btnGenerate").addEventListener("click", async (event) => {
      const { checked } = gId("animation");

      if (validate(event)) {
        jsonSaveWLED.splice(0);

        gId("preview").style.display = "block";
        gId("recreatedImage").innerHTML = "";

        const simulate = gId("simulate");

        if (checked) {
          simulate.style.display = "none";
          await generateAnimation();
        } else {
          simulate.style.display = "flex";
          await generate();
        }
      }
    });

    async function createObjectURL(url) {
      return fetch(url)
        .then((response) => response.arrayBuffer())
        .then((buffer) => {
          const binaryData = new Uint8Array(buffer);
          const base64 = btoa(String.fromCharCode(...binaryData));
          return `data:image/png;base64,${base64}`;
        });
    }

    function loadImage(url) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = (error) => reject(error);
        img.src = url;
      });
    }

    async function generate() {
      const file = gId("source").files[0];

      const { value: images } = gId("images");
      const { value: output } = gId("output");

      show();

      try {
        const response = gId("response");
        const recreatedImage = gId("recreatedImage");

        const urlImage = !images
          ? URL.createObjectURL(file)
          : await createObjectURL(images);
					
        const image = await loadImage(urlImage);
        const { canvas, bri, id, i } = recreate(image);

        await new Promise((resolve) => {
          Object.assign(jsonSendWLED, {
            on: true,
            bri,
            seg: {
              id,
              i,
            },
          });

          resolve();
        });

        const jsonData = JSON.stringify(jsonSendWLED);
        const dataResponse = formatOutput(output, jsonData);

        response.value = dataResponse;
        recreatedImage.appendChild(canvas);
      } catch (error) {
        toast(error, "error");
      } finally {
        hide();
      }
    }

    async function generateAnimation() {
      const file = gId("source").files[0];
      const images = gId("images");
      const response = gId("response");

      const { value: presetName } = gId("name");
      const { value: amount } = gId("frames");
      const { value: output } = gId("output");
      const { value: duration } = gId("duration");

      const { text: imageName, value: imageValue } = images.selectedOptions[0];

      show();

      try {
        const body = new FormData();

        if (!imageValue) {
          body.append("image", file);
        } else {
          const responseImage = await fetch(imageValue);
          const blob = await responseImage.blob();

          const file = new File([blob], imageName, { type: blob.type });

          body.append("image", file);
        }

        const responseFrames = await fetch(
          `https://pixelmagictool.vercel.app/api/wled/frames?amount=${amount}`,
          {
            method: "POST",
            body,
          }
        );

        const { message, frames } = await responseFrames.json();

        if (!responseFrames.ok) {
          toast(message, "error");
          return;
        }

        jsonSaveWLED = [];

        const texteares = [];
        const canvases = [];

        const delay = duration * 1000;

        const promises = frames.map(async (frame, index) => {
          const image = await loadImage(frame);
          const { canvas, bri, id, i } = recreate(image);
          const jsonData = {
            on: true,
            bri,
            seg: { id, i },
          };

          const n = `${presetName} ${index + 1}`;

          texteares.push(`${n}|${JSON.stringify(jsonData)}`);
          canvases.push(canvas);

          jsonSaveWLED.push(Object.assign(jsonData, { n, o: true }));
        });

        await Promise.all(promises);

        const dataResponse = texteares.map((item) => {
          const [presetName, json] = item.split("|");
          const outputFormatted = formatOutput(output, json);
          const comment = ["ha", "curl"].includes(output) ? "#" : "//";

          return `${comment} ${presetName}\n${outputFormatted}`;
        });

        response.value = dataResponse.join("\n");
        toast(message);
        carousel("recreatedImage", canvases, delay);
      } catch (error) {
        toast(error, "error");
      } finally {
        hide();
      }
    }

    function recreate(image) {
      const { value: pattern } = gId("pattern");
      const { value: segmentId } = gId("segments");
      const { value: brightness } = gId("brightness");
      const { value: inputWidth } = gId("width");
      const { value: inputHeight } = gId("height");
      const { checked: resizeImage } = gId("resizeImage");

      const resizeWidth = parseInt(inputWidth);
      const resizeHeight = parseInt(inputHeight);

      const { width: dataWidth, height: dataHeight } =
        gId("segments").selectedOptions[0].dataset;

      const segmentWidth = parseInt(dataWidth);
      const segmentHeight = parseInt(dataHeight);

      const imgWidth = image.naturalWidth;
      const imgHeight = image.naturalHeight;

      const width = resizeImage ? resizeWidth : imgWidth;
      const height = resizeImage ? resizeHeight : imgHeight;

      const overallWidth = resizeImage ? segmentWidth : width;
      const overallHeight = resizeImage ? segmentHeight : height;

      const startX = Math.floor((segmentWidth - width) / 2);
      const startY = Math.floor((segmentHeight - height) / 2);

      const pixelsRef = 48;

      const canvasWidth = overallWidth * pixelsRef;
      const canvasHeight = overallHeight * pixelsRef;

      const fontSize = canvasWidth <= 768 ? 14 : 18;

      const colors = [];

      const { ctx: reference } = createCanvas(width, height);

      reference.drawImage(image, 0, 0, width, height);

      const imageData = reference.getImageData(0, 0, width, height);
      const pixels = imageData.data;

      const { canvas, ctx } = createCanvas(canvasWidth, canvasHeight);

      ctx.fillStyle = "#000000";
      ctx.fillRect(0, 0, canvasWidth, canvasHeight);

      for (let h = 0; h < overallHeight; h++) {
        for (let w = 0; w < overallWidth; w++) {
          let pixelId = h * overallWidth + w + 1;

          let coordinateX = w * pixelsRef;
          let coordinateY = h * pixelsRef;

          let pixelX = w - startX;
          let pixelY = h - startY;

          let hex = "000000";

          if (
            (resizeImage &&
              pixelX >= 0 &&
              pixelX < width &&
              pixelY >= 0 &&
              pixelY < height) ||
            (!resizeImage && w < width && h < height)
          ) {
            let index = resizeImage ? pixelY * width + pixelX : h * width + w;

            let red = pixels[index * 4];
            let green = pixels[index * 4 + 1];
            let blue = pixels[index * 4 + 2];
            let alpha = pixels[index * 4 + 3];

            hex = pixelColor(red, green, blue, alpha);
            let { r, g, b } = hexToRgb(hex);

            colors.push(hex);

            ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
            ctx.fillRect(coordinateX, coordinateY, pixelsRef, pixelsRef);
          } else {
            colors.push("000000");
          }

          ctx.lineWidth = 1;
          ctx.strokeStyle = "#111111";
          ctx.strokeRect(coordinateX, coordinateY, pixelsRef, pixelsRef);

          let offsetX = coordinateX + pixelsRef / 2;
          let offsetY = coordinateY + pixelsRef / 2;

          ctx.font = `${fontSize}px Arial`;
          ctx.fillStyle = hex === "000000" ? "#eeeeee" : "#111111";
          ctx.textAlign = "center";
          ctx.textBaseline = "middle";
          ctx.fillText(pixelId, offsetX, offsetY);
        }
      }

      switch (pattern) {
        case "1":
          i = colors;
          break;
        case "2":
          i = index(colors);
          break;
        case "3":
          i = range(colors);
          break;
      }

      return {
        canvas,
        bri: parseInt(brightness),
        id: parseInt(segmentId),
        i,
      };
    }

    function range(colors) {
      let startIndex = 0;
      let endIndex = 0;
      let currentColor = colors[0];
      let pattern = [];

      colors.forEach((color, index) => {
        if (color !== currentColor) {
          endIndex = index;
          const repetitions = endIndex - startIndex;

          if (repetitions == 1) {
            pattern.push(currentColor);
          } else {
            pattern.push(startIndex, endIndex, currentColor);
          }
          startIndex = index;
        }
        currentColor = color;
      });

      const lastRepetition = colors.length - startIndex;

      if (lastRepetition === 1) {
        pattern.push(currentColor);
      } else {
        pattern.push(startIndex, colors.length, currentColor);
      }

      return pattern;
    }

    function pixelColor(r, g, b, a) {
      const { checked } = gId("transparentImage");
      const { value } = gId("color");

      if (a === 0) {
        if (checked) {
          return value.replace("#", "").toLowerCase();
        }

        return rgbToHex(255, 255, 255);
      }

      return rgbToHex(r, g, b);
    }

    function hexToRgb(hex) {
      hex = hex.replace("#", "");

      if (hex.length === 3) {
        hex = hex.replace(/(.)/g, "$1$1");
      }

      const [r, g, b] = hex
        .match(/.{2}/g)
        .map((component) => parseInt(component, 16));

      return { r, g, b };
    }

    function rgbToHex(r, g, b) {
      const hex = ((r << 16) | (g << 8) | b).toString(16);
      return ("000000" + hex).slice(-6);
    }

    function index(colors) {
      const pattern = [];
      colors.forEach((led, index) => {
        pattern.push(index, led);
      });

      return pattern;
    }

    function createCanvas(width, height) {
      const canvas = d.createElement("canvas");

      canvas.width = width;
      canvas.height = height;

      const ctx = canvas.getContext("2d", { willReadFrequently: true });

      ctx.imageSmoothingQuality = "high";
      ctx.imageSmoothingEnabled = false;

      return { canvas, ctx };
    }

    function formatOutput(output, json) {
      return output === "ha"
        ? yaml(json)
        : output === "curl"
        ? curl(json)
        : json;
    }

    function downloadFile(text, filename, output) {
      let mimeType;
      let fileExtension;
      let response;

      switch (output) {
        case "json":
          mimeType = "application/json";
          fileExtension = "json";
          break;
        case "ha":
          mimeType = "application/x-yaml";
          fileExtension = "yaml";
          break;
        case "curl":
          mimeType = "text/plain";
          fileExtension = "txt";
          break;
      }

      const blob = new Blob([text], { type: mimeType });
      const url = URL.createObjectURL(blob);

      const anchorElement = d.createElement("a");
      anchorElement.href = url;
      anchorElement.download = `${filename}.${fileExtension}`;

      anchorElement.click();

      URL.revokeObjectURL(url);
    }

    function yaml(jsonData) {
      const { value: device } = gId("device");
      const { value: friendly_name } = gId("friendlyName");
      const { value: unique_id } = gId("uniqueId");
      const { value: hostname } = gId("hostname");

      if (device) {
        const yamlData = {
          switches: {
            [device]: {
              friendly_name,
              unique_id,
              command_on: `curl -X POST "http://${hostname}/json/state" -d '${jsonData}' -H "Content-Type: application/json"`,
              command_off: `curl -X POST "http://${hostname}/json/state" -d '{"on":false}' -H "Content-Type: application/json"`,
            },
          },
        };

        return convertToYaml(yamlData);
      }
    }

    function curl(jsonData) {
      const { value: hostname } = gId("hostname");
      return `curl -X POST "http://${hostname}/json/state" -d '${jsonData}' -H "Content-Type: application/json"`;
    }

    function convertToYaml(obj) {
      function processValue(value, indentationLevel) {
        if (typeof value === "object" && !Array.isArray(value)) {
          return processObject(value, indentationLevel + 1);
        } else {
          return value;
        }
      }

      function processObject(obj, indentationLevel = 0) {
        const indent = "  ".repeat(indentationLevel);
        const lines = Object.entries(obj).map(([key, value]) => {
          if (typeof value === "object" && !Array.isArray(value)) {
            return `${indent}${key}:\n${processObject(
              value,
              indentationLevel + 1
            )}`;
          } else {
            return `${indent}${key}: ${processValue(value, indentationLevel)}`;
          }
        });

        return lines.join("\n");
      }

      return processObject(obj);
    }

    function toast(
      message,
      type = "success",
      duration = 2000,
      hideElement = "preview"
    ) {
      const hide = gId(hideElement);
      const toast = d.createElement("div");
      const wait = 100;

      toast.style.animation = "fadeIn";
      toast.style.animationDuration = `${duration + wait}ms`;
      toast.style.animationTimingFunction = "linear";

      toast.classList.add("toast", type);

      const body = d.createElement("span");
      body.classList.add("toast-body");

      body.textContent = message;

      toast.appendChild(body);

      const progress = d.createElement("div");
      progress.classList.add("toast-progress");

      progress.style.animation = "progress";
      progress.style.animationDuration = `${duration + wait}ms`;
      progress.style.animationDelay = "0s";

      toast.appendChild(progress);

      gId("toast-container").appendChild(toast);

      setTimeout(() => {
        toast.style.opacity = 0;
        setTimeout(() => {
          toast.remove();
        }, wait);
      }, duration);

      if (type === "error") {
        hide.style.display = "none";
      }
    }

    function carousel(id, images, delay = 3000) {
      let index = 0;
      const carousel = d.createElement("div");
      carousel.classList.add("carousel");

      images.forEach((canvas, i) => {
        if (i === index) {
          carousel.appendChild(canvas);
        } else {
          canvas.style.display = "none";
          carousel.appendChild(canvas);
        }
      });

      const container = gId(id);
      container.innerHTML = "";
      container.appendChild(carousel);

      function next() {
        images[index].style.display = "none";
        index++;
        if (index >= images.length) {
          index = 0;
        }

        images[index].style.display = "block";
        loop();
      }

      function previous() {
        images[index].style.display = "none";
        index--;
        if (index < 0) {
          index = images.length - 1;
        }

        images[index].style.display = "block";
        loop();
      }

      let timeoutId;
      function loop() {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          next();
          if (index === 0) {
            carousel.scrollTo(0, 0);
          }
        }, delay);
      }
      loop();

      carousel.addEventListener("mouseover", () => {
        clearTimeout(timeoutId);
      });

      carousel.addEventListener("mouseout", () => {
        loop();
      });
    }

    function show() {
      const overlay = gId("overlay");
      overlay.classList.add("loading");
      overlay.style.display = "block";
      overlay.style.cursor = "not-allowed";

      d.body.style.overflow = "hidden";
    }

    function hide() {
      const overlay = gId("overlay");
      overlay.classList.remove("loading");
      overlay.style.display = "none";
      overlay.style.cursor = "default";

      d.body.style.overflow = "auto";
    }

    function validate(event) {
      event.preventDefault();

      const form = gId("formGenerate");
      const inputs = form.querySelectorAll("input, select, textarea");

      let isValid = true;

      const browserLanguage = navigator.language || navigator.userLanguage;
      const messageRequired =
        browserLanguage === "pt-br"
          ? "Este campo é obrigatório"
          : "This field is required";

      inputs.forEach((input) => {
        const parent = input.closest("[validate]");

        if (!parent) {
          return;
        }

        let isVisible = true;

        let tempParent = parent;
        while (tempParent !== d.body) {
          const parentStyles = window.getComputedStyle(tempParent);
          if (
            parentStyles.display === "none" ||
            parentStyles.display === "hidden"
          ) {
            isVisible = false;
            break;
          }
          tempParent = tempParent.parentNode;
        }

        if (
          isVisible &&
          (!input.checkValidity() ||
            (input.type === "file" && input.files.length === 0))
        ) {
          input.classList.add("invalid");

          const errorMessage =
            input.dataset.errorMessage ||
            input.validationMessage ||
            messageRequired;

          let errorElement = parent.querySelector(".error-message");

          if (!errorElement) {
            errorElement = d.createElement("div");
            errorElement.classList.add("error-message");
            parent.appendChild(errorElement);
          }

          errorElement.innerText = errorMessage;
          isValid = false;
        } else {
          input.classList.remove("invalid");
          const errorElement = parent.querySelector(".error-message");

          if (errorElement) {
            parent.removeChild(errorElement);
          }
        }
      });

      return isValid;
    }
  </script>
</html>