CSS Gradient builder

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

<!DOCTYPE html>
<html >
<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/ */
#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/ */
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();
    }
}

This awesome code ( CSS Gradient builder ) is write by Darren Loasby, you can se more from this user in the personal repository

You can find the original code on Codepen.io

2018 © Darren Loasby