Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <canvas id='c'></canvas>
<script type="text/javascript" src="https://codepen.io/Twanneman/pen/AtEjF"></script>
/* 
Cool lightning simulation in canvas. You can interact with it in the following ways:

Click to add new points.
Double Click to remove an existing point.
Click and drag on a point to change its position.
*/
              
            
!

CSS

              
                body {
    margin: 0;
    padding: 0;
}
canvas {
    position: absolute;
}
              
            
!

JS

              
                // forked from akm2's "Lightning Points (Lightning 2)" https://jsdo.it/akm2/amk0
/**
 * Using PerlinNoise class
 * Using Point class
 * @see https://jsdo.it/akm2/fhMC
 */

var DRAG_POINT_NUM = 4;
var DRAG_POINT_MAX_NUM = 8;
var CHILD_NUM = 2;
var BACKGROUND_COLOR = 'rgba(0, 15, 20, 0.8)';

// Color
var H = 195;
var S = 100;
var L_MAX = 85;
var L_MIN = 45;

var canvas;
var context;
var dragPoints = [];
var mouse = new Point();
var baseLine;
var lightningLine;

// alias
var random = Math.random;
var floor  = Math.floor;

function init() {
    document.body.style.backgroundColor = BACKGROUND_COLOR;
    canvas = document.getElementById('c');
    
    document.addEventListener('resize', resize, false);
    resize();
        
    var i;
    
    for (i = 0; i < DRAG_POINT_NUM; i++) {
        dragPoints.push(new DragPoint(canvas.width * random(), canvas.height * random()));
    }
    
    var baseNoiseOpts      = { base: 100000, amplitude: 0.6, speed: 0.02 };
    var lightningNoiseOpts = { base: 90, amplitude: 0.2, speed: 0.05 };
    var childNoiseOpts     = { base: 60, amplitude: 0.8, speed: 0.08 };
    
    baseLine      = new NoiseLine(8,  baseNoiseOpts);
    lightningLine = new NoiseLine(16, lightningNoiseOpts);
    for (i = 0; i < CHILD_NUM; i++) {
        lightningLine.createChild(childNoiseOpts);
    }
    
    // *** Debug
    baseLine.debug = true;
    // *********
    
    document.addEventListener('mousemove', mouseMove, false);
    document.addEventListener('mousedown', mouseDown, false);
    document.addEventListener('mouseup', mouseUp, false);
    document.addEventListener('dblclick', doubleClick, false);
    document.addEventListener('keydown', keyDown, false);
    
    setInterval(loop, 1000 / 30);
}

function resize(e) {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    context = canvas.getContext('2d');
    context.lineCap = 'round';
}

function mouseMove(e) {
    mouse.set(e.clientX, e.clientY);
    
    var hit = false;
    for (var i = 0, len = dragPoints.length; i < len; i++) {
        if (dragPoints[i].hitTest(mouse)) {
            hit = true;
            break;
        }
    }
    document.body.style.cursor = hit ? 'pointer' : 'default';
}

function mouseDown(e) {
    var i, len;
    
    for (i = 0, len = dragPoints.length; i < len; i++) {
        if (dragPoints[i].dragStart(mouse)) return;
    }
    
    for (i = 0; i < len; i++) {
        if (dragPoints[i].hitTest(mouse)) {
            if (len > 1) dragPoints.splice(i, 1);
            return;
        }
    }
    
    if (len < DRAG_POINT_MAX_NUM) {
        dragPoints.push(new DragPoint(e.clientX, e.clientY));
    } else {
        for (i = 0; i < len - 2; i++) {
            dragPoints[i].kill();
        }
    }
}

function mouseUp(e) {
    for (var i = 0, len = dragPoints.length; i < len; i++) {
        dragPoints[i].dragEnd(mouse);
    }
}

function doubleClick(e) {
    var len = dragPoints.length;
    if (len < 3) return;
    for (var i = 0; i < len; i++) {
        if (dragPoints[i].hitTest(mouse)) {
            dragPoints[i].kill();
            return;
        }
    }
}

function keyDown(e) {
    if (e.keyCode === 68) { // d key
        Debug.enabled = !Debug.enabled;
    }
}

function loop() {
    context.globalCompositeOperation = 'source-over';
    context.fillStyle = BACKGROUND_COLOR;
    context.fillRect(0, 0, canvas.width, canvas.height);
    
    context.globalCompositeOperation = 'lighter';
    
    var i, len, p;
    
    var controls = [];
    for (i = 0, len = dragPoints.length; i < len; i++) {
        p = dragPoints[i];
        p.update();
        p.alpha = p.hitTest(mouse) ? 0.75 : 0.2;
        p.draw(context);
        if (p.dead) {
            dragPoints.splice(i, 1);
            i--;
            len--;
            continue;
        }
        if (!p.dying) {
            controls.push(p);
        }
    }
    
    // 原点からの距離でソート
    controls.sort(sortPoints);
    
    baseLine.update(controls);
    
    lightningLine.update(baseLine.points);
    drawLightningBlur(lightningLine, 50, 30);
    drawLightningLine(lightningLine, 0.75, 1, 1, 5);
    drawLightningCap(lightningLine);
    
    lightningLine.eachChild(function(child, i) {
        drawLightningLine(child, 0, 1, 0, 4);
        drawLightningBlur(child, 50, 30);
    });
    
    Color.l = randomRange(L_MIN, L_MAX);
    
    // * debug
    Debug.exec();
}

// Array sort callback
function sortPoints(p1, p2) {
    return p1.length() - p2.length();
}


// Lightning draw methods

function drawLightningLine(line, maxAlpha, minAlpha, maxLineW, minLineW) {
    context.beginPath();
    context.strokeStyle = Color.setAlphaToString(randomRange(minAlpha, maxAlpha));
    context.lineWidth   = randomRange(minLineW, maxLineW);
    line.eachPoints(function(p, i) {
        context[i === 0 ? 'moveTo' : 'lineTo'](p.x, p.y);
    });
    context.stroke();
}

function drawLightningBlur(line, blur, maxSize) {
    var dist;
    context.save();
    context.fillStyle = 'rgba(0, 0, 0, 1)';
    context.shadowBlur = blur;
    context.shadowColor = Color.setAlphaToString();
    context.beginPath();
    line.eachPoints(function(p, i, len) {
        dist = len > 1 ? p.distance(this[i === len - 1 ? i - 1 : i + 1]) : 0;
        if (dist > maxSize) dist = maxSize;
        context.moveTo(p.x + dist, p.y);
        context.arc(p.x, p.y, dist, 0, Math.PI * 2, false);
    });
    context.fill();
    context.restore();
}

function drawLightningCap(line) {
    var points = line.points;
    var p, radius, gradient;
    for (var i = 0, len = points.length; i < len; i += len - 1) {
        p = points[i];
        radius = randomRange(3, 8);
        gradient = context.createRadialGradient(p.x, p.y, radius / 3, p.x, p.y, radius);
        gradient.addColorStop(0, Color.setAlphaToString(1));
        gradient.addColorStop(1, Color.setAlphaToString(0));
        context.fillStyle = gradient;
        context.beginPath();
        context.arc(p.x, p.y, radius, 0, Math.PI * 2, false);
        context.fill();
    }
}


// Helper

function randomRange(min, max) {
    return random() * (max - min) + min;
}


(function(window) {
    //PerlinNoise.useClassic = true;
    var perlinNoise = new PerlinNoise();
    perlinNoise.octaves(3);
    
    /**
     * NoiseLine
     * 
     * @param segmentsNum 制御点間の分割数
     * @param noiseOptions ノイズのオプション
     */
    function NoiseLine(segmentsNum, noiseOptions) {
        this.segmentsNum = segmentsNum;

        this.noiseOptions = extend({
            base: 30,
            amplitude: 0.5,
            speed: 0.002,
            offset: 0
        }, noiseOptions);

        this.points = [];
        this.lineLength = 0;
        this.children = [];
    }

    NoiseLine.prototype = {
        createChild: function(noiseOptions) {
            var child = new NoiseLineChild(this, noiseOptions || this.noiseOptions);
            this.children.push(child);
            return child;
        },
        
        eachChild: function(callback) {
            var children = this.children;
            for (var i = 0, len = children.length; i < len; i++) {
                callback.call(children, children[i], i, len);
            }
        },

        eachPoints: function(callback) {
            var points = this.points;
            for (var i = 0, len = points.length; i < len; i++) {
                callback.call(points, points[i], i, len);
            }
        },

        update: function(controls) {
            var i, len;
            
            // 振り幅の係数として使用するため制御点を全て直線で結んだ距離を取得する
            var lineLength = 0;
            for (i = 0, len = controls.length; i < len; i++) {
                if (i === len - 1) break;
                lineLength += controls[i].distance(controls[i + 1]);
            }
            this.lineLength = lineLength;
            
            // スプライン曲線を生成してノイズを適用
            this.noise(spline(controls, this.segmentsNum), lineLength);
            
            // *** Debug
            if (Debug.enabled && this.debug) {
                this.eachPoints(function(p, i) {
                    Debug.addCommand(function() { Debug.point(p.x, p.y, 3, 'blue'); });
                });
            }
            // *********
            
            // 最短距離を取得
            this.points = shortest(this.points);
            
            // *** Debug
            if (Debug.enabled && this.debug) {
                this.eachPoints(function(p, i) {
                    Debug.addCommand(function() { Debug.point(p.x, p.y, 3, 'red'); });
                });
            }
            // *********
            
            // 子を更新
            var children = this.children;
            for (i = 0, len = children.length; i < len; i++) {
                children[i].update();
            }
        },
        
        noise: function(bases, range) {
            var pointsOld = this.points;
            var points = this.points = [];
            
            var opts = this.noiseOptions;
            var base = opts.base;
            var amp = opts.amplitude;
            var speed = opts.speed;
            var offset = opts.offset += random() * speed;
            
            var p, next, angle, sin, cos, av, ax, ay, bv, bx, by, m, px, py;
            
            for (var i = 0, len = bases.length; i < len; i++) {
                p = bases[i];
                next = i === len - 1 ? p : bases[i + 1];

                angle = next.subtract(p).angle();
                sin = Math.sin(angle);
                cos = Math.cos(angle);
                
                av = range * perlinNoise.noise(i / base - offset, offset) * 0.5 * amp;
                ax = av * sin;
                ay = av * cos;

                bv = range * perlinNoise.noise(i / base + offset, offset) * 0.5 * amp;
                bx = bv * sin;
                by = bv * cos;

                m = Math.sin(Math.PI * (i / (len - 1)));

                px = p.x + (ax - bx) * m;
                py = p.y - (ay - by) * m;

                points.push(pointsOld.length ? pointsOld.shift().set(px, py) : new Point(px, py));
                
                // *** Debug
                if (Debug.enabled && this.debug) {
                    Debug.addCommand((function(p, angle) {
                        return function() {
                            context.save();
                            context.translate(p.x, p.y);
                            context.rotate(angle);
                            this.line(0, 0, 15999, 9990, 'pink', 1);
                            this.point(0, 0, 2, 'pink');
                            context.restore();
                        };
                    })(p, angle));
                }
                // *********
            }
        }
    };


    /**
     * NoiseLineChild
     * 
     * @super NoiseLine
     */
    function NoiseLineChild(parent, noiseOptions) {
        this.parent = lightningLine;
        this._lastChangeTime = 0;
        NoiseLine.call(this, 0, noiseOptions || lightningLine.noiseOptions);
    }

    NoiseLineChild.prototype = extend({}, NoiseLine.prototype, {
        startStep: 0,
        endStep: 0,
        
        // Clear super class methods
        createChild: undefined,
        eachChild: undefined,

        update: function() {
            var parent = this.parent;
            var plen = parent.points.length;

            // 一定時間ごと, あるいは親のポイントの数が子の終了ステップ位置を下回った場合に始点と終点の親からの取得位置を更新する
            var currentTime = new Date().getTime();
            if (
                currentTime - this._lastChangeTime > 10000 * random()
                || plen < this.endStep
            ) {
                var stepMin = floor(plen / 10);
                var startStep = this.startStep = floor(random() * floor(plen / 3 * 2));
                this.endStep = startStep + stepMin + floor(random() * (plen - startStep - stepMin) + 1);
                this._lastChangeTime = currentTime;
            }

            // 親のポイント配列から取得範囲を切り出す
            var range = parent.points.slice(this.startStep, this.endStep);
            var rangeLen = range.length;
            
            // 範囲からスプライン曲線の制御点を取得する
            var sep = 2; // 分割数
            var seg = (rangeLen - 1) / sep;
            var controls = [];
            var i, j;
            for (i = 0; i <= sep; i++) {
                j = Math.floor(seg * i);
                controls.push(range[j]);
            }
            
            // *** Debug
            if (Debug.enabled) {
                (function() {
                    for (var i = 0, len = controls.length - 1, p, n; i < len; i++) {
                        p = controls[i];
                        Debug.addCommand((function(p) {
                            return function() { Debug.point(p.x, p.y, 3, 'yellow'); };
                        })(p));
                    }
                })();
            }
            // *********
            
            // スプライン曲線を生成
            var base = spline(controls, Math.floor(rangeLen / 3));
            
            // *** Debug
            if (Debug.enabled) {
                (function() {
                    for (var i = 0, len = base.length - 1, p, n; i < len; i++) {
                        p = base[i];
                        n = base[i + 50];
                        Debug.addCommand((function(p, n) {
                            return function() { Debug.line(p.x, p.y, n.x, n.y, 'yellow', 1); };
                        })(p, n));
                    }
                })();
            }
            // *********
            
            // ノイズを適用
            this.noise(base, controls[0].distance(controls[2]));
            // 最短距離を取得
            this.points = shortest(this.points);
        }
    });
    
    function spline(controls, segmentsNum) {
        // スプライン補完用に配列の前後にラインの始点, 終点の参照をそれぞれ複製する
        controls.unshift(controls[0]);
        controls.push(controls[controls.length - 1]);

        // スプライン曲線のポイントを取得
        var points = [];
        var p0, p1, p2, p3, t;
        var j;
        for (var i = 0, len = controls.length - 3; i < len; i++) {
            p0 = controls[i];
            p1 = controls[i + 1];
            p2 = controls[i + 2];
            p3 = controls[i + 3];
            
            for (j = 0; j < segmentsNum; j++) {
                t = (j + 1) / segmentsNum;
                
                points.push(new Point(
                    catmullRom(p0.x, p1.x, p2.x, p3.x, t),
                    catmullRom(p0.y, p1.y, p2.y, p3.y, t)
                ));
            }
        }

        // 補完用に追加した参照を削除
        controls.pop();
        // 削除のついでに描画の始点として追加
        points.unshift(controls.shift());
        
        return points;
    }
    
    /**
     * Catmull-Rom Spline Curve
     * 
     * @see http://l00oo.oo00l.com/blog/archives/264
     */
    function catmullRom(p0, p1, p2, p3, t) {
        var v0 = (p2 - p0) / 2;
        var v1 = (p3 - p1) / 2;
        return (2 * p1 - 2 * p2 + v0 + v1) * t * t * t
            + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t * t + v0 * t + p1;
    }
    
    function shortest(bases) {
        var points = [bases[0]];
        var p, j, p2, dist, minDist, k;
        for (var i = 0, len = bases.length; i < len; i++) {
            p = bases[i];

            minDist = Infinity;
            k = -1;
            for (j = i; j < len; j++) {
                if ((p2 = bases[j]) !== p && (dist = p.distance(p2)) < minDist) {
                    minDist = dist;
                    k = j;
                }
            }
            if (k < 0) break;

            points.push(bases[k]);
            i = k - 1;
        }

        return points;
    }
    
    window.NoiseLine = NoiseLine;
    
})(window);


/**
 * DragPoint
 * 
 * @super Point https://jsdo.it/akm2/fhMC
 */
function DragPoint(x, y) {
    this.x = x;
    this.y = y;
    this.radius = 50;
    this.alpha = 0.2;
    this.dragging = false;
    this.dying = false;
    this.dead = false;
    
    this._v = new Point(randomRange(-3, 3), randomRange(-3, 3));
    
    this._mouse = null;
    this._latestMouse = new Point();
    this._mouseDist = null;
    
    this._currentAlpha = 0;
    this._currentRadius = 0;
}

DragPoint.prototype = extend({}, Point.prototype, {
    hitTest: function(mouse) {
        return this.distance(mouse) < this.radius;
    },

    dragStart: function(mouse) {
        if (this.hitTest(mouse)) {
            this._mouse = mouse;
            this._mouseDist = this.subtract(this._mouse);
            this.dragging = true;
        }
        return this.dragging;
    },

    dragEnd: function() {        
        if (this.dragging && this._latestMouse) {
            this._v.set(this._mouse.subtract(this._latestMouse));
        }
        this.dragging = false;
        this._mouse = this._mouseDist = null;
    },
    
    kill: function() {
        this.dying = true;
        this.radius = 0;
    },

    update: function(mouse) {
        var v = this._v;
        
        if (this._mouse) {
            this.set(this._mouse.add(this._mouseDist));
            this._latestMouse.set(this._mouse);
        } else {
            this.offset(v);
            v.x *= 0.97;
            v.y *= 0.97;
            
            var vlen = v.length();
            if (vlen > 30) {
                v.normalize(30);
            } else if (vlen < 1) {
                v.normalize(1);
            }
        }
        
        var radius = this.radius;

        if (this.x < radius) {
            this.x = this.radius;
            if (v.x < 0) v.x *= -1;
        } else if (this.x > canvas.width - radius) {
            this.x = canvas.width - radius;
            if (v.x > 0) v.x *= -1;
        }

        if (this.y < radius) {
            this.y = radius;
            if (v.y < 0) v.y *= -1;
        } else if (this.y > canvas.height - radius) {
            this.y = canvas.height - radius;
            if (v.y > 0) v.y *= -1;
        }
        
        var d;
        // Alpha
        d = this.alpha - this._currentAlpha;
        if ((d < 0 ? -d : d) > 0.001) this._currentAlpha += d * 0.1;
        // Radius
        d = radius - this._currentRadius;
        if ((d < 0 ? -d : d) > 0.01) {
            this._currentRadius += d * 0.35;
        } else if (this.dying) {
            this.dead = true;
        }
        this._currentRadius *= randomRange(0.9, 1);
    },

    draw: function(ctx) {
        var radius = this._currentRadius;
        var gradient = ctx.createRadialGradient(this.x, this.y, radius / 3, this.x, this.y, radius);
        gradient.addColorStop(0, Color.setAlphaToString(this._currentAlpha));
        gradient.addColorStop(1, Color.setAlphaToString(0));
        ctx.fillStyle = gradient;
        ctx.beginPath();
        ctx.moveTo(this.x + radius, this.y);
        ctx.arc(this.x, this.y, radius, 0, Math.PI * 2, false);
        ctx.fill();
    }
});


/**
 * Color
 */
var Color = new function() {
    this.h = H;
    this.s = S;
    this.l = L_MAX;
    
    this.setAlphaToString = function(alpha) {
        if (typeof alpha === 'undefined' || alpha === null) {
            return 'hsl(' + this.h + ', ' + this.s + '%, ' + this.l + '%)';
        }
        return 'hsla(' + this.h + ', ' + this.s + '%, ' + this.l + '%, ' + alpha + ')';
    };
};


// Init

window.onload = function() {
    init();
};


// メインスクリプトここまで

//-----------------------------------------
// DEBUG
//-----------------------------------------

var Debug = {
    enabled: false,
    _commands: [],
    
    addCommand: function(fn) {
        if (this.enabled) this._commands.push(fn);
    },
    
    exec: function() {
        if (this.enabled) {
            var commands = this._commands;
            for (var i = 0, len = commands.length; i < len; i++) {
                commands[i].call(this);
            }
            this._commands = [];
        }
    },
    
    line: function(x1, y1, x2, y2, color, lineWidth) {
        if (this.enabled) {
            context.save();
            context.globalCompositeOperation = 'source-over';
            context.strokeStyle = color;
            context.lineWidth = !lineWidth ? 1 : lineWidth;
            context.beginPath();
            context.moveTo(x1, y1);
            context.lineTo(x2, y2);
            context.stroke();
            context.restore();
        }
    },
    
    point: function(x, y, radius, color) {
        if (this.enabled) {
            context.save();
            context.globalCompositeOperation = 'source-over';
            context.fillStyle = color;
            context.beginPath();
            context.arc(x, y, radius, 0, Math.PI * 2, false);
            context.fill();
            context.restore();
        }
    }
};

              
            
!
999px

Console