CSS Gradient builder

In this example below you will see how to do a CSS Gradient builder with some HTML / CSS and Javascript

Experiment to draw gradients interactively.Also has support for midpoints. Those are exported to CSS using experimental proposed syntax.

Thumbnail
This awesome code was written by darrenloasby, you can see more from this user in the personal repository.
You can find the original code on Codepen.io
Copyright darrenloasby ©

Technologies

  • HTML
  • CSS
  • JavaScript
<!DOCTYPE html>
<html lang="en" >

<head>
  <meta charset="UTF-8">
  <title>CSS Gradient builder</title>
  
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">

  
      <style>
      /* NOTE: The styles were added inline because Prefixfree needs access to your styles and they must be inlined if they are on local disk! */
      #canvasholder
{
  width: 600px;
  height: 400px;
  margin: 5px;
  flex-grow: 1;
  flex-shrink: 1;
}
#stopcolors {
  flex-grow: 0;
  flex-shrink: 0;
  width: 100px;
}

#rendering-linear, #rendering-radial
{
  margin: 10pp;
  border: dashed 1px black;
  background: black;
  flex-grow: 1;
  flex-shrink: 1;
  width: 600px;
  height: 400px;
  margin: 5px;
}

.columncontainer {
  display: flex;
  flex-direction: column;
}

.container {
  display: flex;
}

#left {
  /* width: 700px; */
  /* min-width: 700px; */
}
#right {
  min-width: 600px;
}
/* #content {
  flex-wrap: wrap; 
} */

#canvasholder {
  border: 1px dashed black;
}

.description, .css {
  height: 200px;
}

.css p {
  height: 40%;
  border: 1px dashed black;
  background-color: rgba(28, 30, 32, 0.5);
  color: yellow;
  overflow: scroll;
}
    </style>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>

</head>

<body>

  <h1>Interactive CSS gradients</h1>
<div class="container" id="content">
  <div id="left" class="columncontainer">
    <div class="description">
      <p>Manipulate the gradient by dragging the endpoints. You can add stops by clicking on the gradient line and move them around.</p>
      <p>There's experimental support for midpoints as well.</p>
      <p>A list of colorstops is provided on the right so you can delete them or change their color.</p>
      <p>We will do the math to generate the right CSS gradient and provide example rendering to make sure it matches.</p>
    </div>
    <div class="container">
      <div id="canvasholder"></div>
      <ol id="stopcolors"></ol>
    </div>
  </div>
  <div class="columncontainer" id="right">
    <div class="css">
      <p id="linearGradientSyntax"></p>
      <p id="radialGradientSyntax"></p>
    </div>
    <div class="container">
      <div id="rendering-linear"></div>
      <div id="rendering-radial"></div>
    </div>
  </div>
</div>
  <script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
<script src='https://cabanier.github.io/pixi.js'></script>

  

    <script  src="js/index.js"></script>




</body>

</html>

/*Downloaded from https://www.codeseek.co/darrenloasby/css-gradient-builder-aBbrOW */
#canvasholder
{
  width: 600px;
  height: 400px;
  margin: 5px;
  flex-grow: 1;
  flex-shrink: 1;
}
#stopcolors {
  flex-grow: 0;
  flex-shrink: 0;
  width: 100px;
}

#rendering-linear, #rendering-radial
{
  margin: 10pp;
  border: dashed 1px black;
  background: black;
  flex-grow: 1;
  flex-shrink: 1;
  width: 600px;
  height: 400px;
  margin: 5px;
}

.columncontainer {
  display: flex;
  flex-direction: column;
}

.container {
  display: flex;
}

#left {
  /* width: 700px; */
  /* min-width: 700px; */
}
#right {
  min-width: 600px;
}
/* #content {
  flex-wrap: wrap; 
} */

#canvasholder {
  border: 1px dashed black;
}

.description, .css {
  height: 200px;
}

.css p {
  height: 40%;
  border: 1px dashed black;
  background-color: rgba(28, 30, 32, 0.5);
  color: yellow;
  overflow: scroll;
}

/*Downloaded from https://www.codeseek.co/darrenloasby/css-gradient-builder-aBbrOW */
var $canvasholder;
var lastW = 0;
var lastH = 0;
$(window).load(function () {
  $canvasholder = $("#canvasholder");
  reload();
  $(window).resize(reload);
});
function reload() {
    var stage = new PIXI.Stage(0x66FF99, true);
    var width = $canvasholder.width();
    var height = $canvasholder.height();

    var renderer = new PIXI.CanvasRenderer(width, height);
    $canvasholder.children().remove();
    $canvasholder.get(0).appendChild(renderer.view);
    $(renderer.view).mousemove(animate);
console.log("reload");
    transparentcanvas = document.createElement("canvas");
    var ctx = transparentcanvas.getContext("2d");
    ctx.width = 100; ctx.height = 100;

    backdropcanvas = document.createElement("canvas");

    ctx = backdropcanvas.getContext("2d");
    ctx.width = width;ctx.height = height;
    backdropcanvas.width = width;backdropcanvas.height = height;
    ctx.fillStyle = "rgba(255, 255, 255,.5)";
    ctx.fillRect(0, 0, width, height);
    var textureBackdrop = PIXI.Texture.fromCanvas(backdropcanvas);

    var backdrop = new PIXI.Sprite(textureBackdrop);

    var gradientbar = new PIXI.Sprite(PIXI.Texture.fromCanvas(transparentcanvas));
    gradientbar.setInteractive(true);

    backdrop.anchor.x = 0;
    backdrop.anchor.y = 0;
    backdrop.position.x = 0;
    backdrop.position.y = 0;
    backdrop.width = width;
    backdrop.height = height;
    backdrop.setInteractive(true);
    var startGrad = new PIXI.Point(10, 200);
    var endGrad = new PIXI.Point(width -10, 200);

    var gradientLine = null;
    var bumpline = false;
    var gradientStart = null;
    var gradientEnd = null;

    function isClose(a, b, d) {
        return Math.abs(a.x - b.x) < 10 && Math.abs(a.y - b.y) < 10;
    }

    function distance(x1, y1, x2, y2) {
        return Math.sqrt(Math.pow(x1-x2, 2)+ Math.pow(y1-y2, 2));
    }
    
    backdrop.mousedown = backdrop.touchstart = function (data) {
        if(bumpline)
            return;

        this.isdown = true;

        if (endGrad && isClose(endGrad, data.global, 10)) {
            this.isMoveEnd = true;
        }

        if (startGrad && isClose(startGrad, data.global, 10)) {
            this.isMoveStart = true;
        }
        else
            endGrad = data.global.clone();

        if (!this.isMoveEnd)
            startGrad = data.global.clone();
    }

    // set the mouseup and touchend callback..
    backdrop.mouseup = backdrop.touchend = function (data) {
        if(bumpline || this.isdown == undefined)
            return;

        this.isdown = false;
        if (this.isMoveStart)
            startGrad = data.global.clone();
        else
            endGrad = data.global.clone();
        this.isMoveStart = false;
        this.isMoveEnd = false;
    }

    backdrop.mousemove = function (data) {
        if(bumpline)
            return;

        if (!this.isdown)
            return;

        if (this.isMoveStart)
            startGrad = data.global.clone();
        else
            endGrad = data.global.clone();
    }

    var colors = [[0,0,0], [255,255,0]];
    var pos = [0, 1];
    var mid = [.5];
    var midGraphics = [];
    var AddedStops = false;
    var draggingStop = false;

    gradientbar.mouseout = function (data) {
        if(backdrop.isdown || draggingStop)
            return;

        bumpline = false;
    }
    gradientbar.mouseover = function (data) {
        if(backdrop.isdown)
            return;

        bumpline = true;
        requestAnimFrame(animate);
    }

    gradientbar.mousedown = function (data) {
        if(!bumpline || backdrop.isdown)
            return;
        bumpline = true;
    }

    stage.addChild(backdrop);
    //////////////////////////////////////////////////////////////
    function componentToHex(c) {
        var hex = Math.round(c).toString(16);
        return hex.length == 1 ? "0" + hex : hex;
    }

    function rgbToHex(r, g, b) {
        return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
    }

    function AddColorStops() {
        var list = $("#stopcolors");
        list.children().remove();
        for(var x = 0; x < colors.length; x++) {
            var li = document.createElement("li");
            list.append(li);
            var stop= document.createElement("input");
          try{
            stop.type = "color";
          }catch(e){
            stop.style.width = "75px";
          }
            li.appendChild(stop);
            var color = colors[x];
            stop.value = rgbToHex(color[0], color[1], color[2]);
            stop.colorvalue = color;
            $(stop).change(function(){
                var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(this.value);
                this.colorvalue[0] = parseInt(result[1], 16);
                this.colorvalue[1] = parseInt(result[2], 16);
                this.colorvalue[2] = parseInt(result[3], 16);
                requestAnimFrame(animate);
            });
            if((x==0)||(x==colors.length-1))
                continue;
            var b = document.createElement("button");
            b.textContent = "delete";
            b.pos = x;
            li.appendChild(b);
            $(b).click(function(){
                colors.splice(this.pos, 1);
                pos.splice(this.pos, 1);
                mid.splice(this.pos, 1);
                requestAnimFrame(animate);
                AddColorStops();
            });
        }
    }

    //////////////////////////////////////////////////////////////


    requestAnimFrame(animate);


   
    function drawGradient() {
        var l = Math.sqrt(Math.pow(startGrad.x - endGrad.x, 2) + Math.pow(startGrad.y - endGrad.y, 2));
        var a = Math.atan2(startGrad.y - endGrad.y, startGrad.x - endGrad.x) + Math.PI;;
        ctx.save();
        ctx.translate(startGrad.x, startGrad.y);
        ctx.rotate(a);

        var c1 = colors[0];
        ctx.fillStyle = "rgb(" + c1[0] + ", " + c1[1] + ", " + c1[2] + ")";
        ctx.fillRect(-1000, -1000, 2000, 2000);

        for(var t = 1; t < pos.length; t++) {
            var startLength = pos[t-1];
            var endLength = pos[t];
            var c1 = colors[t-1];
            var c2 = colors[t];
            var multiplier = (endLength - startLength);
            var exp = Math.log(.5)/Math.log(mid[t-1]);

            for(x = 0; x < 256; x++) {
                var s = "rgb(";
                s += Math.floor(c1[0] + Math.pow(x/255, exp)*(c2[0]-c1[0])) + ",";
                s += Math.floor(c1[1] + Math.pow(x/255, exp)*(c2[1]-c1[1])) + ",";
                s += Math.floor(c1[2] + Math.pow(x/255, exp)*(c2[2]-c1[2])) + ")";

                ctx.fillStyle = s;
                ctx.fillRect((startLength+x/256*multiplier)*l, -1000, 2000, 2000);
            }
        }
        var c2 = colors[colors.length-1];
        ctx.fillStyle = "rgb(" + c2[0] + ", " + c2[1] + ", " + c2[2] + ")";
        ctx.fillRect(endLength*l, -1000, 2000, 2000);

        ctx.restore();
    }

    function updateMidpoint(index, length){
        var xpos = (pos[index] + mid[index] * (pos[index+1]-pos[index])) * length;
        midGraphics[index].position.x = xpos;
    }

    function addMidpoint(art, index, length) {
            var stop = new PIXI.Graphics();
            var xpos = (pos[index] + mid[index] * (pos[index+1]-pos[index])) * length;

            stop.lineStyle(2, 0x000000, 1);
            stop.beginFill(0xffffff, 1);
            stop.moveTo(0, 0);
            stop.lineTo(5, -5);
            stop.lineTo(5, -12);
            stop.lineTo(-5, -12);
            stop.lineTo(-5, -5);
            stop.lineTo(0, 0);
            stop.endFill();
            
            art.addChild(stop);
            midGraphics[index] = stop;
            updateMidpoint(index, length)

            stop.hitArea = new PIXI.Rectangle(-10, -20, +20, 24);
            stop.setInteractive(true);
            stop.mousedown = function (data) {
                this.hit = true;
                draggingStop = true;
                this.startPoint = data.global;
            }

            stop.mousemove = function (data) {
                if(!this.hit)
                    return;
                var l = Math.sqrt(Math.pow(startGrad.x - endGrad.x, 2) + Math.pow(startGrad.y - endGrad.y, 2));
                var draggedLength = Math.sqrt(Math.pow(startGrad.x - data.global.x, 2) + Math.pow(startGrad.y - data.global.y, 2));

                var newpos = draggedLength/l;

                if((index > 0) && newpos<=pos[index]+.05)
                    newpos = pos[index]+.05;
                else
                if((index < mid.length) && newpos>=pos[index+1]-.05)
                    newpos = pos[index+1]-.05;


                mid[index] = (newpos - pos[index])/(pos[index+1] - pos[index]);
                stop.position.x = newpos * length;
            }
            stop.mouseout = stop.mouseup = function (data) {
                this.hit = false;
                draggingStop = false;
            }
    }

    function addStop(art, index, length) {
            var stop = new PIXI.Graphics();
            var xpos = pos[index] * length;

            stop.lineStyle(2, 0x000000, 1);
            stop.beginFill(0xffffff, 1);
            stop.moveTo(0, 0);
            stop.lineTo(10, 10);
            stop.lineTo(10, 25);
            stop.lineTo(-10, 25);
            stop.lineTo(-10, 10);
            stop.lineTo(0, 0);
            stop.endFill();
            stop.position.x = xpos;
            art.addChild(stop);

            stop.hitArea = new PIXI.Rectangle(-12, 0, +24, 28);
            stop.setInteractive(true);

            stop.mousedown = function (data) {
                this.hit = true;
                draggingStop = true;
                this.startPoint = data.global;
            }

            stop.mousemove = function (data) {
                if(!this.hit)
                    return;

                var l = Math.sqrt(Math.pow(startGrad.x - endGrad.x, 2) + Math.pow(startGrad.y - endGrad.y, 2));
                var draggedLength = Math.sqrt(Math.pow(startGrad.x - data.global.x, 2) + Math.pow(startGrad.y - data.global.y, 2));
                prevpos = pos[index];
                pos[index] = draggedLength/l;

                if((index > 0) && pos[index]<=pos[index-1]+.01)
                    pos[index] = pos[index-1]+.01;
                else
                if((index < pos.length-1) && pos[index]>=pos[index+1]-.01)
                    pos[index] = pos[index+1]-.01;

                if(pos[index]<0)
                    pos[index] = 0;
                if(pos[index]>1)
                    pos[index] = 1;

                stop.position.x = pos[index] * length;

                if(index>0)
                    updateMidpoint(index-1, l);
                if(index<pos.length-1)
                    updateMidpoint(index, l);
            }

            stop.mouseout = stop.mouseup = function (data) {
                this.hit = false;
                draggingStop = false;
            }
    }

    function drawGradientLine() {
        drawGradient();
        if (!gradientStart) {
            AddColorStops();
            gradientLine = new PIXI.Graphics();
            stage.addChild(gradientLine);

            gradientStart = new PIXI.Graphics();
            gradientStart.lineStyle(2, 0x000000, 1);
            gradientStart.beginFill(0xffffff, 1);
            gradientStart.drawCircle(0, 0, 10);
            gradientStart.endFill();
            stage.addChild(gradientStart);

            gradientEnd = new PIXI.Graphics();
            gradientEnd.lineStyle(2, 0x000000, 1);
            gradientEnd.beginFill(0xffffff, 1);
            gradientEnd.drawCircle(0, 0, 10);
            gradientEnd.endFill();
            stage.addChild(gradientEnd);

            stage.addChild(gradientbar);
        }

        gradientLine.beginFill(0xffffff, 1);
        var l = Math.sqrt(Math.pow(startGrad.x - endGrad.x, 2) + Math.pow(startGrad.y - endGrad.y, 2));
        var a = Math.atan2(startGrad.y - endGrad.y, startGrad.x - endGrad.x) + Math.PI;
        gradientLine.clear();
        gradientLine.lineStyle(2, 0x000000, 1);
        gradientLine.beginFill(0xffffff, 1);
        if (bumpline)
            gradientLine.drawRect(0, -6, l, 12);
        else
            gradientLine.drawRect(0, -3, l, 6);

        gradientLine.endFill();

        if (bumpline && ! AddedStops) {
            stage.removeChild(gradientLine);
            stage.addChild(gradientLine);

            for(var x = 0; x < pos.length; x++) 
                addStop(gradientLine, x, l);

            for(var x = 0; x < mid.length; x++) 
                addMidpoint(gradientLine, x, l);
            AddedStops = true;
        }
        else
        if (!bumpline && AddedStops) {
            while(gradientLine.children.length)
                gradientLine.removeChild(gradientLine.getChildAt(0));

            stage.removeChild(gradientLine);
            stage.addChildAt(gradientLine, 1);
            AddedStops = false;
        }

        gradientLine.rotation = a;
        gradientLine.position = startGrad;

        gradientbar.position = startGrad;

        if (bumpline) {
            gradientbar.mousedown = function (data) {
                var l = Math.sqrt(Math.pow(startGrad.x - endGrad.x, 2) + Math.pow(startGrad.y - endGrad.y, 2));
                var clickedLength = Math.sqrt(Math.pow(startGrad.x - data.global.x, 2) + Math.pow(startGrad.y - data.global.y, 2))/l;
                if((clickedLength<pos[0])||(clickedLength>pos[pos.length-1]))
                    return;

                for(var x = 0; x < pos.length; x++) {
                    if(pos[x]>clickedLength) {
                        pos.splice(x, 0, clickedLength);
                        var c = [];
                        if(x==0)
                            c = colors[x].slice(0);
                        else {
                            var offset = (pos[x] - pos[x-1])/(pos[x+1] - pos[x-1]);
                            for(var i = 0; i < 3; i++)
                                c[i] = colors[x-1][i] + offset*(colors[x][i] - colors[x-1][i]);
                        }

                        colors.splice(x, 0, c);
                        mid.splice(x, 0, .5);

                        break;
                    }
                }

                if(x == pos.length) {
                    pos[pos.length] = clickedLength;

                    mid[mid.length-1] = .5;
                    colors[pos.length-1] = colors[pos.length-2].slice(0);
                } 

                while(gradientLine.children.length)
                    gradientLine.removeChild(gradientLine.getChildAt(0));
                for(var x = 0; x < pos.length; x++) 
                    addStop(gradientLine, x, l);
                for(var x = 0; x < mid.length; x++) 
                    addMidpoint(gradientLine, x, l);

                AddColorStops();
                requestAnimFrame(animate);
            };
            gradientbar.hitArea = new PIXI.Rectangle(-10, -15, l+20, 42);
        }
        else {
            gradientbar.mousedown = function (data) {};
            gradientbar.hitArea = new PIXI.Rectangle(8, -6, l-16, 12);
        }

        gradientbar.rotation = a;

        gradientStart.position = startGrad;
        gradientEnd.position = endGrad;
    }

    function CreateGradientText(){
        if(!startGrad)
            return;

        // move vector (x1, y1) (x2, y2) to (x3 y3)
        // n = (x2 - x1, y2 - y1)/ dist(vector) = (a, b)
        // a = (x3 - x1, y3 - y1) = (c, d)
        // r = (b * c - a * d )
        // k = (r * b, -r * a)
        var dist = distance(startGrad.x, startGrad.y, endGrad.x, endGrad.y);
        var nx = (startGrad.x - endGrad.x) / dist;
        var ny = (startGrad.y - endGrad.y) / dist;
        var ax = (300 - startGrad.x);
        var ay = (200 - startGrad.y);
        var r =  ny * ax - nx * ay;
        var kx = r * ny;
        var ky = -r * nx;

        // find start and end of gradient on the gradient line
        var StartX = startGrad.x + kx;
        var StartY = startGrad.y + ky;
        var EndX = endGrad.x + kx;
        var EndY = endGrad.y + ky;

        // calculate the length of the gradient line
        // What side does the gradient line cross?
        var angle = Math.atan2(StartY - EndY, StartX - EndX);

        var slope = Math.tan(angle);
        var d = 200 - slope * 300;

        // angle of internal rect
        var rectAngle = Math.atan2(300, 200);
        var rectLength = distance(300, 200, 0, 0);

        var reducedAngle =angle;//Math.abs(angle);
        if(reducedAngle<0)
            reducedAngle += 2*Math.PI;
        while(reducedAngle > Math.PI/2)
            reducedAngle -= Math.PI/2;
        // x = 0
        var crossY = Math.abs(d);
        // y = 0
        var crossX = Math.abs(-d / slope);
        var length = 0;
        if(crossX<crossY) {
            if(Math.PI/2  - reducedAngle > rectAngle) {// intersect top/bottom
                length = rectLength * Math.cos(reducedAngle - rectAngle);
            } else { // intersect left/right
                length = rectLength * Math.cos(Math.PI/2 - reducedAngle - rectAngle);
            }
        } else if(reducedAngle > rectAngle) {// intersect top/bottom
            length = rectLength * Math.cos(reducedAngle - rectAngle);
        } else { // intersect left/right
            length = rectLength * Math.cos(Math.PI/2 - reducedAngle - rectAngle);
        }

        // this is the length used on the gradient line
        var dist = distance(EndX, EndY, StartX, StartY);
        var distStart = distance(300, 200, StartX, StartY);
        var distEnd = distance(300, 200, EndX, EndY);

        // is the middle of the box between the 2 outer stops?
        if(Math.abs(dist - (distStart+distEnd))<.5) {
            var offset = length - distStart;
        } else // is the end between middle and start?
        if(Math.abs(distEnd + dist - distStart)<.5)
            var offset = length - distStart;
        else
            var offset = length + distStart;

        offset /= length*2;
        var multiplier = dist/length/2;

        var a = angle / Math.PI * 180 - 90;
        if(a<0) 
            a = 360+a;
        var deg = Math.round(a) + "deg";
        var stops = "";

        for(var x = 0; x < colors.length; x++){
            if(x && mid[x-1] != .5) {
                var s1 = offset + multiplier*pos[x-1];
                var s2 = offset + multiplier*pos[x];
                if (x > 0)
                  stops += ", ";
                stops += Math.round((s1 + mid[x-1] * (s2 - s1))*100) + "%";
            }
            if (x > 0)
              stops += ", ";
            stops += "rgb(" + Math.round(colors[x][0]) + ", " + Math.round(colors[x][1]) + ", " + Math.round(colors[x][2]) + ")"
            stops += " " + Math.round((offset + multiplier*pos[x])*100) + "%";
        }
        
        var linear = "linear-gradient(" + deg + ", " + stops + ")";
        var radial = "radial-gradient(circle, " + stops + ")";

        $("#linearGradientSyntax").text("background: " + linear);
        $("#radialGradientSyntax").text("background: " + radial);

        $("#rendering-linear").css("background", linear);
        $("#rendering-radial").css("background", radial);
    }
    function animate() {
        if (startGrad && endGrad)
            drawGradientLine();

        renderer.render(stage);
        CreateGradientText();
    }
}

Comments