I'm working on an interactive colouring page. The user is presented with an SVG image, and when they select a colour from the swatch, they can click on a section of the image to change its fill colour.

So far, I have that part, the image exports, and a custom colour picker down:

https://codepen.io/candidee/pen/abxvPKV?editors=1100

#main {}

#sidebar {
  float: left;
  width: 60px;
}

path {
  transition: fill 0.3s ease;
  /* Add transition property for the fill */
}
<!-- Source: https://gist.github.com/widged/4545199, MIT Licensed -->

<div id="sidebar">

  <!-- Colour swatches -->
  <svg id="swatches" width="60px" height="200px" viewBox="-4 -4 60 200" style="stroke: #000;stroke-width: 2; stroke-opacity: 0.1">
     <rect style="fill:#000"    x="0"  y="0" width="20" height="20"/>
     <rect style="fill:#AD5C51" x="25" y="0" width="20" height="20"/>
     <rect style="fill:#F4CBB2" x="0"  y="25" width="20" height="20"/>
     <rect style="fill:#f00"    x="25" y="25" width="20" height="20"/>
     <rect style="fill:#7DBBE6" x="0"  y="50" width="20" height="20"/>
     <rect style="fill:#9CDAF1" x="25" y="50" width="20" height="20"/>
     <rect style="fill:#C3E4D8" x="0"  y="75" width="20" height="20"/>
     <rect style="fill:#fff"    x="25" y="75" width="20" height="20"/>
    
    <!-- Add a custom swatch for color picker -->
     <defs>
       <linearGradient id="rainbowGradient" x1="0%" y1="0%" x2="100%" y2="0%">
         <stop offset="0%" stop-color="#ff0000"></stop>
         <stop offset="16.66%" stop-color="#ff7f00"></stop>
         <stop offset="33.33%" stop-color="#ffff00"></stop>
         <stop offset="49.99%" stop-color="#00ff00"></stop>
         <stop offset="66.66%" stop-color="#0000ff"></stop>
         <stop offset="83.33%" stop-color="#4b0082"></stop>
         <stop offset="100%" stop-color="#9400d3"></stop>
       </linearGradient>
     </defs>
     <rect id="colorPickerSwatch" x="0" y="100" width="20" height="20" style="fill:url(#rainbowGradient);stroke:#000;stroke-width:2"/>

    <!-- Colour of outline that surrounds the selected colour -->
     <rect id="selection" style="stroke:#0000ff; stroke-opacity: 1;fill:none" x="0" y="0" width="20" height="20"/>
    
  </svg>
</div>

<button id="downloadBtn">Download SVG</button>

<button id="downloadPngBtn">Download PNG</button>


<div id="main">

  <!--  SVG Drawing starts here -->
  <svg id="octocat" xmlns="http://www.w3.org/2000/svg" width="400px" height="340px" viewBox="-60 0 420 320" style="fill:#fff;stroke: #000; stroke-opacity: 0.1">
    <path id="puddle" d="m296.94 295.43c0 20.533-47.56 37.176-106.22 37.176-58.67 0-106.23-16.643-106.23-37.176s47.558-37.18 106.23-37.18c58.66 0 106.22 16.65 106.22 37.18z"/>
    <path class="shadow-legs" d="m161.85 331.22v-26.5c0-3.422-.619-6.284-1.653-8.701 6.853 5.322 7.316 18.695 7.316 18.695v17.004c6.166.481 12.534.773 19.053.861l-.172-16.92c-.944-23.13-20.769-25.961-20.769-25.961-7.245-1.645-7.137 1.991-6.409 4.34-7.108-12.122-26.158-10.556-26.158-10.556-6.611 2.357-.475 6.607-.475 6.607 10.387 3.775 11.33 15.105 11.33 15.105v23.622c5.72.98 11.71 1.79 17.94 2.4z"/>
    <path class="shadow-legs" d="m245.4 283.48s-19.053-1.566-26.16 10.559c.728-2.35.839-5.989-6.408-4.343 0 0-19.824 2.832-20.768 25.961l-.174 16.946c6.509-.025 12.876-.254 19.054-.671v-17.219s.465-13.373 7.316-18.695c-1.034 2.417-1.653 5.278-1.653 8.701v26.775c6.214-.544 12.211-1.279 17.937-2.188v-24.113s.944-11.33 11.33-15.105c0-.01 6.13-4.26-.48-6.62z"/>
    <path id="cat" d="m378.18 141.32l.28-1.389c-31.162-6.231-63.141-6.294-82.487-5.49 3.178-11.451 4.134-24.627 4.134-39.32 0-21.073-7.917-37.931-20.77-50.759 2.246-7.25 5.246-23.351-2.996-43.963 0 0-14.541-4.617-47.431 17.396-12.884-3.22-26.596-4.81-40.328-4.81-15.109 0-30.376 1.924-44.615 5.83-33.94-23.154-48.923-18.411-48.923-18.411-9.78 24.457-3.733 42.566-1.896 47.063-11.495 12.406-18.513 28.243-18.513 47.659 0 14.658 1.669 27.808 5.745 39.237-19.511-.71-50.323-.437-80.373 5.572l.276 1.389c30.231-6.046 61.237-6.256 80.629-5.522.898 2.366 1.899 4.661 3.021 6.879-19.177.618-51.922 3.062-83.303 11.915l.387 1.36c31.629-8.918 64.658-11.301 83.649-11.882 11.458 21.358 34.048 35.152 74.236 39.484-5.704 3.833-11.523 10.349-13.881 21.374-7.773 3.718-32.379 12.793-47.142-12.599 0 0-8.264-15.109-24.082-16.292 0 0-15.344-.235-1.059 9.562 0 0 10.267 4.838 17.351 23.019 0 0 9.241 31.01 53.835 21.061v32.032s-.943 11.33-11.33 15.105c0 0-6.137 4.249.475 6.606 0 0 28.792 2.361 28.792-21.238v-34.929s-1.142-13.852 5.663-18.667v57.371s-.47 13.688-7.551 18.881c0 0-4.723 8.494 5.663 6.137 0 0 19.824-2.832 20.769-25.961l.449-58.06h4.765l.453 58.06c.943 23.129 20.768 25.961 20.768 25.961 10.383 2.357 5.663-6.137 5.663-6.137-7.08-5.193-7.551-18.881-7.551-18.881v-56.876c6.801 5.296 5.663 18.171 5.663 18.171v34.929c0 23.6 28.793 21.238 28.793 21.238 6.606-2.357.474-6.606.474-6.606-10.386-3.775-11.33-15.105-11.33-15.105v-45.786c0-17.854-7.518-27.309-14.87-32.3 42.859-4.25 63.426-18.089 72.903-39.591 18.773.516 52.557 2.803 84.873 11.919l.384-1.36c-32.131-9.063-65.692-11.408-84.655-11.96.898-2.172 1.682-4.431 2.378-6.755 19.25-.80 51.38-.79 82.66 5.46z"/>
    <path id="face" d="m258.19 94.132c9.231 8.363 14.631 18.462 14.631 29.343 0 50.804-37.872 52.181-84.585 52.181-46.721 0-84.589-7.035-84.589-52.181 0-10.809 5.324-20.845 14.441-29.174 15.208-13.881 40.946-6.531 70.147-6.531 29.07-.004 54.72-7.429 69.95 6.357z"/>
    <path id="eyes" d="m160.1 126.06 c0 13.994-7.88 25.336-17.6 25.336-9.72 0-17.6-11.342-17.6-25.336 0-13.992 7.88-25.33 17.6-25.33 9.72.01 17.6 11.34 17.6 25.33z m94.43 0 c0 13.994-7.88 25.336-17.6 25.336-9.72 0-17.6-11.342-17.6-25.336 0-13.992 7.88-25.33 17.6-25.33 9.72.01 17.6 11.34 17.6 25.33z"/>
    <path id="pupils" d="m154.46 126.38 c0 9.328-5.26 16.887-11.734 16.887s-11.733-7.559-11.733-16.887c0-9.331 5.255-16.894 11.733-16.894 6.47 0 11.73 7.56 11.73 16.89z m94.42 0 c0 9.328-5.26 16.887-11.734 16.887s-11.733-7.559-11.733-16.887c0-9.331 5.255-16.894 11.733-16.894 6.47 0 11.73 7.56 11.73 16.89z"/>
    <circle id="nose" cx="188.5" cy="148.56" r="4.401"/>
    <path id="mouth" d="m178.23 159.69c-.26-.738.128-1.545.861-1.805.737-.26 1.546.128 1.805.861 1.134 3.198 4.167 5.346 7.551 5.346s6.417-2.147 7.551-5.346c.26-.738 1.067-1.121 1.805-.861s1.121 1.067.862 1.805c-1.529 4.324-5.639 7.229-10.218 7.229s-8.68-2.89-10.21-7.22z"/>
    <path id="octo" d="m80.641 179.82 c0 1.174-1.376 2.122-3.07 2.122-1.693 0-3.07-.948-3.07-2.122 0-1.175 1.377-2.127 3.07-2.127 1.694 0 3.07.95 3.07 2.13z m8.5 4.72 c0 1.174-1.376 2.122-3.07 2.122-1.693 0-3.07-.948-3.07-2.122 0-1.175 1.377-2.127 3.07-2.127 1.694 0 3.07.95 3.07 2.13z m5.193 6.14 c0 1.174-1.376 2.122-3.07 2.122-1.693 0-3.07-.948-3.07-2.122 0-1.175 1.377-2.127 3.07-2.127 1.694 0 3.07.95 3.07 2.13z m4.72 7.08 c0 1.174-1.376 2.122-3.07 2.122-1.693 0-3.07-.948-3.07-2.122 0-1.175 1.377-2.127 3.07-2.127 1.694 0 3.07.95 3.07 2.13z m5.188 6.61 c0 1.174-1.376 2.122-3.07 2.122-1.693 0-3.07-.948-3.07-2.122 0-1.175 1.377-2.127 3.07-2.127 1.694 0 3.07.95 3.07 2.13z m7.09 5.66 c0 1.174-1.376 2.122-3.07 2.122-1.693 0-3.07-.948-3.07-2.122 0-1.175 1.377-2.127 3.07-2.127 1.694 0 3.07.95 3.07 2.13z m9.91 3.78 c0 1.174-1.376 2.122-3.07 2.122-1.693 0-3.07-.948-3.07-2.122 0-1.175 1.377-2.127 3.07-2.127 1.694 0 3.07.95 3.07 2.13z m9.87 0 c0 1.174-1.376 2.122-3.07 2.122-1.693 0-3.07-.948-3.07-2.122 0-1.175 1.377-2.127 3.07-2.127 1.694 0 3.07.95 3.07 2.13z m10.01 -1.64 c0 1.174-1.376 2.122-3.07 2.122-1.693 0-3.07-.948-3.07-2.122 0-1.175 1.377-2.127 3.07-2.127 1.694 0 3.07.95 3.07 2.13z"/>
    <path id="drop" d="m69.369 186.12l-3.066 10.683s-.8 3.861 2.84 4.546c3.8-.074 3.486-3.627 3.223-4.781z"/>
  </svg>
  <!--  SVG Drawing ends here -->
</div>


<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>


<script>
  //// ✨ COLOUR PICKER CUSTOM COLOR ✨ ////

  // Function to handle color picker selection
  function handleColorPickerSelection(event) {
    // Check if a color picker input element already exists
    if (!document.getElementById('colorPickerInput')) {
      // Create a color picker input element
      var input = document.createElement('input');
      input.type = 'color';
      input.id = 'colorPickerInput'; // Assign an id to the input element

      // Add event listener for color change
      input.addEventListener('change', function() {
        // Set the current fill to the selected color
        var selectedColor = input.value;
        _currentFill = 'fill:' + selectedColor;

        // Remove the input element from the body
        document.body.removeChild(input);
      });

      // Append the input element to the body
      document.body.appendChild(input);

      // Trigger click event on color picker input
      input.click();
    }
  }

  // Add event listener to the custom swatch for color picker
  document.getElementById('colorPickerSwatch').addEventListener('click', handleColorPickerSelection);




  //// ✨ CHANGING THE FILL COLOUR DYNAMICALLY ✨ ////

  // default selected colour when page is loaded
  var _currentFill = "fill:#fff";

  //  Replace  #octocat with the HTML ID of your illustration SVG
  $("#octocat").click(function(event) {
    $(event.target).attr('style', _currentFill);
  })
  var $swatches = $("#swatches");
  $swatches.click(function(event) {
    $swatch = $(event.target);
    loc = [parseInt($swatch.attr('x'), 10), parseInt($swatch.attr('y'), 10)]
    $("#selection", $swatches).attr('x', loc[0]);
    $("#selection", $swatches).attr('y', loc[1]);
    _currentFill = $swatch.attr('style');;
  })




  //// ✨ DOWNLOAD BUTTON: SVG ✨ ////

  // Function to download the SVG file
  function downloadSVG() {
    // Get the SVG element
    var svg = document.getElementById('octocat'); //  Replace  'octocat' with the HTML ID of your illustration SVG

    // Get the serialized XML of the SVG
    var svgData = new XMLSerializer().serializeToString(svg);

    // Create a blob with the SVG data
    var blob = new Blob([svgData], {
      type: 'image/svg+xml'
    });

    // Create a link element
    var link = document.createElement('a');
    link.href = window.URL.createObjectURL(blob);

    //  Set the file name
    link.download = 'colored_octocat.svg';

    // Append the link to the body
    document.body.appendChild(link);

    // Trigger the download
    link.click();

    // Remove the link from the body
    document.body.removeChild(link);
  }

  // Add event listener to the download button
  document.getElementById('downloadBtn').addEventListener('click', downloadSVG);




  //// ✨ DOWNLOAD BUTTON: PNG ✨ ////

  // Function to download the SVG as PNG
  function downloadPNG() {
    // Get the SVG element
    var svg = document.getElementById('octocat'); //  Replace  'octocat' with the HTML ID of your illustration SVG

    // Get the dimensions of the SVG
    var width = svg.getAttribute('width');
    var height = svg.getAttribute('height');

    // Create a canvas element
    var canvas = document.createElement('canvas');
    canvas.width = parseInt(width, 10);
    canvas.height = parseInt(height, 10);

    // Get the canvas context
    var context = canvas.getContext('2d');

    // Create a new Image object
    var img = new Image();

    // Create a blob with the SVG data
    var svgData = new XMLSerializer().serializeToString(svg);
    var blob = new Blob([svgData], {
      type: 'image/svg+xml'
    });

    // Create a URL for the blob
    var url = URL.createObjectURL(blob);

    // Set the source of the image to the URL
    img.src = url;

    // When the image is loaded, draw it on the canvas
    img.onload = function() {
      context.drawImage(img, 0, 0);

      // Convert the canvas to data URL
      var dataURL = canvas.toDataURL('image/png');

      // Create a link element
      var link = document.createElement('a');
      link.href = dataURL;

      //  Set the file name
      link.download = 'colored_octocat.png';

      // Append the link to the body
      document.body.appendChild(link);

      // Trigger the download
      link.click();

      // Remove the link from the body
      document.body.removeChild(link);
    };
  }

  // Add event listener to the download button
  document.getElementById('downloadPngBtn').addEventListener('click', downloadPNG);
</script>


<p>Credits: <a href="https://gist.github.com/1007813">Octocat SVG</a>.</p>

<p>Tested on Safari, Chrome, Firefox.</p>

The problem is: I want to give users some kind of indicator of the current colour. I'm thinking some kind of little circle that follows the mouse. When the user clicks on the a swatch colour, the circle's fill/background colour should match the current value of _currentFill.

I tried querying GPT with not much luck. Here's what was suggested - The circle follows, but the colour doesn't change or update.

// Function to handle color picker selection
function handleColorPickerSelection(event) {
  // Check if a color picker input element already exists
  if (!document.getElementById('colorPickerInput')) {
    // Create a color picker input element
    var input = document.createElement('input');
    input.type = 'color';
    input.id = 'colorPickerInput'; // Assign an id to the input element

    // Add event listener for color change
    input.addEventListener('change', function() {
      // Set the current fill to the selected color
      var selectedColor = input.value;
      _currentFill = 'fill:' + selectedColor;

      // Remove the input element from the body
      document.body.removeChild(input);
    });

    // Append the input element to the body
    document.body.appendChild(input);

    // Trigger click event on color picker input
    input.click();
  }
}

// Add event listener to the custom swatch for color picker
document.getElementById('colorPickerSwatch').addEventListener('click', handleColorPickerSelection);

// Function to update the position of the color indicator circle
function updateColorIndicator(event) {
  var indicator = document.getElementById('colorIndicator');

  // Update the position of the indicator circle based on the mouse cursor
  indicator.setAttribute('cx', event.clientX - 8); // Adjust for SVG viewBox and circle size
  indicator.setAttribute('cy', event.clientY - 8); // Adjust for SVG viewBox and circle size
}

// Add event listener to update the color indicator position when the mouse moves
document.addEventListener('mousemove', updateColorIndicator);
<div id="sidebar">
  <!-- Colour swatches -->
  <svg id="swatches" width="60px" height="200px" viewBox="-4 -4 60 200" style="stroke: #000;stroke-width: 2; stroke-opacity: 0.1">
    <!-- Rainbow gradient -->
    <defs>
      <linearGradient id="rainbowGradient" x1="0%" y1="0%" x2="100%" y2="0%">
        <stop offset="0%" stop-color="red"/>
        <stop offset="20%" stop-color="orange"/>
        <stop offset="40%" stop-color="yellow"/>
        <stop offset="60%" stop-color="green"/>
        <stop offset="80%" stop-color="blue"/>
        <stop offset="100%" stop-color="violet"/>
      </linearGradient>
    </defs>
    <!-- Rect for rainbow gradient -->
    <rect id="colorPickerSwatch" x="0" y="100" width="20" height="20" fill="url(#rainbowGradient)" style="stroke:#000;stroke-width:2"/>
    
    <!-- Indicator circle -->
    <circle id="colorIndicator" cx="0" cy="0" r="5" fill="transparent" stroke="#000" stroke-width="2"/>
  </svg>
</div>

1

There are 1 best solutions below

3
mplungjan On

Have a play with this.

Your mousemove script hogged the CPU so the picker did not show and you were missing

indicator.style.fill = input.value;

const indicator = document.getElementById('colorIndicator');
const colorPickerSwatch = document.getElementById('colorPickerSwatch');

// Function to hide the indicator
function hideIndicator() {
  indicator.style.display = 'none';
}

// Function to show the indicator
function showIndicator() {
  indicator.style.display = '';
}

// Add mouseover and mouseout event listeners to the swatch
colorPickerSwatch.addEventListener('mouseover', hideIndicator);
colorPickerSwatch.addEventListener('mouseout', showIndicator);

// Function to handle color picker selection
function handleColorPickerSelection(event) {
  // Create a color picker input element if it does not exist
  if (!document.getElementById('colorPickerInput')) {
    var input = document.createElement('input');
    input.type = 'color';
    input.id = 'colorPickerInput';

    // Add event listener for color change
    input.addEventListener('change', function() {
      // Set the color of the indicator to the selected color
      indicator.style.fill = input.value;
      
      // Show the indicator if it was hidden
      showIndicator();

      // Remove the input element from the body
      document.body.removeChild(input);
    });

    // Append the input element to the body and trigger the color picker
    document.body.appendChild(input);
    input.click();
  }
}

// Add event listener to the swatch for the color picker
colorPickerSwatch.addEventListener('click', handleColorPickerSelection);


// Add event listener to the custom swatch for color picker
document.getElementById('colorPickerSwatch').addEventListener('click', handleColorPickerSelection);

function updateColorIndicator(event) {
  // Check if the mouse is over the colorPickerSwatch
  if (!colorPickerSwatch.contains(event.target)) {
    // Update the position of the indicator circle based on the mouse cursor
    indicator.setAttribute('cx', event.clientX - 8); // Adjust for SVG viewBox and circle size
    indicator.setAttribute('cy', event.clientY - 8); // Adjust for SVG viewBox and circle size
  }
}

document.addEventListener('mousemove', updateColorIndicator);
<div id="sidebar">
  <!-- Colour swatches -->
  <svg id="swatches" width="60px" height="200px" viewBox="-4 -4 60 200" style="stroke: #000;stroke-width: 2; stroke-opacity: 0.1">
    <!-- Rainbow gradient -->
    <defs>
      <linearGradient id="rainbowGradient" x1="0%" y1="0%" x2="100%" y2="0%">
        <stop offset="0%" stop-color="red"/>
        <stop offset="20%" stop-color="orange"/>
        <stop offset="40%" stop-color="yellow"/>
        <stop offset="60%" stop-color="green"/>
        <stop offset="80%" stop-color="blue"/>
        <stop offset="100%" stop-color="violet"/>
      </linearGradient>
    </defs>
    <!-- Rect for rainbow gradient -->
    <rect id="colorPickerSwatch" x="0" y="100" width="20" height="20" fill="url(#rainbowGradient)" style="stroke:#000;stroke-width:2"/>
    
    <!-- Indicator circle -->
    <circle id="colorIndicator" cx="0" cy="0" r="5" fill="transparent" stroke="#000" stroke-width="2"/>
  </svg>
</div>