<canvas id="drawing_canvas"></canvas>
body {
    background-color: #222;
    margin: 0;
}

#drawing_canvas {
    position: absolute;
    margin: auto;
    width: 512px;
    height: 512px;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    border: 1px solid #555
}
// canvas settings
var viewWidth = 512,
    viewHeight = 512,
    centerX = viewWidth * 0.5,
    centerY = viewHeight * 0.5,
    drawingCanvas = document.getElementById("drawing_canvas"),
    ctx,
    timeStep = (1/30),
    time = 0;

var ribbon,
    frontGradient,
    backGradient;

function initDrawingCanvas() {
    drawingCanvas.width = viewWidth;
    drawingCanvas.height = viewHeight;
    ctx = drawingCanvas.getContext('2d');

    ctx.translate(0.5, 0.5);

    frontGradient = ctx.createLinearGradient(0, 0, viewWidth, 0);
    frontGradient.addColorStop(0.0, '#999');
    frontGradient.addColorStop(0.5, '#fff');
    frontGradient.addColorStop(1.0, '#999');

    backGradient = ctx.createLinearGradient(0, 0, viewWidth, 0);
    backGradient.addColorStop(0.0, '#555');
    backGradient.addColorStop(0.5, '#777');
    backGradient.addColorStop(1.0, '#555');
}

function initCurves() {

    var curves = [],
        rotations = 12,
        dy = (viewHeight + 84) / rotations;

    for (var i = 0; i <= rotations; i++) {
        var c,
            startX = centerX,
            startY = viewHeight - dy * i + 84,
            controlX = (i % 2) * viewWidth,
            controlY = startY;

        c = new Curve(
            new Point(startX, startY),
            new Point(controlX, controlY),
            new Point(controlX, controlY - dy),
            new Point(startX, startY - dy)
        );

        curves.push(c);
    }

    ribbon = new BezierRibbon(curves, 42);
}

function update() {
//    ribbon.updateTime(timeStep);
    ribbon.currentTime = Math.min(time, ribbon.totalTime);
    ribbon.startOffset = Math.max(time - 1.5, 0);

    time %= (ribbon.totalTime + 1.5);

//    console.log(time % (ribbon.totalTime));
//    ribbon.currentTime = Math.min(time, ribbon.totalTime);
//    ribbon.startOffset = Math.max(ribbon.currentTime - 3, 0);
}

function draw() {
//    ctx.fillStyle = '#000';
    ctx.clearRect(0, 0, viewWidth, viewHeight);

    ribbon.draw();
}

window.onload = function() {
    initDrawingCanvas();
    initCurves();
    requestAnimationFrame(loop);
};

function loop() {
    update();
    draw();
    time += timeStep;
    requestAnimationFrame(loop);
}

////////////////////////////
// class and utils
////////////////////////////
BezierRibbon = function(curves, width) {
    this.curves = curves;
    this.width = width;
    this.halfWidth = this.width * 0.5;
    this.totalTime = curves.length; // 1.0 per curve
    this.currentTime = 0;
    this.startOffset = 0;

    // point cache
    this.tp0 = new Point();
    this.tp1 = new Point();
    this.tp2 = new Point();
    this.tp3 = new Point();
};
BezierRibbon.prototype = {
    draw:function() {
        // debug draw
//        this.curves.forEach(function(c) {
//            c.draw();
//        });

        var step = 1e-2,
            index = 0,
            dx,
            curve;

        for (var t = this.startOffset; t < this.currentTime; t += step) {
            index = t | 0;
            curve = this.curves[index];
            curve.interpolate(t - index, this.tp0);
            curve.interpolate(t + step - index, this.tp1);

            this.tp0.translate(0, -this.halfWidth);
            this.tp1.translate(0, -this.halfWidth);
            this.tp2.copy(this.tp1).translate(0, this.width);
            this.tp3.copy(this.tp0).translate(0, this.width);

            dx = this.tp1.x - this.tp0.x;

            ctx.strokeStyle = ctx.fillStyle = dx >= 0 ? frontGradient : backGradient;
            ctx.globalCompositeOperation = dx >= 0 ? 'source-over' : 'destination-over';
            ctx.lineWidth = 1.5;

            ctx.beginPath();
            ctx.moveTo(this.tp0.x, this.tp0.y);
            ctx.lineTo(this.tp1.x, this.tp1.y);
            ctx.lineTo(this.tp2.x, this.tp2.y);
            ctx.lineTo(this.tp3.x, this.tp3.y);
            ctx.closePath();
            ctx.fill();
            ctx.stroke();
        }
    }
};

Curve = function(p0, p1, p2, p3) {
    this.p0 = p0;
    this.p1 = p1;
    this.p2 = p2;
    this.p3 = p3;
};
Curve.prototype = {
    interpolate:function(t, p) {
        p = p || new Point();

        var nt = (1 - t);

        p.x = nt * nt * nt * this.p0.x + 3 * nt * nt * t * this.p1.x + 3 * nt * t * t * this.p2.x + t * t * t * this.p3.x;
        p.y = nt * nt * nt * this.p0.y + 3 * nt * nt * t * this.p1.y + 3 * nt * t * t * this.p2.y + t * t * t * this.p3.y;

        return p;
    },
    draw:function() {
        ctx.strokeStyle = "#f00";
        ctx.beginPath();
        ctx.moveTo(this.p0.x, this.p0.y);
        ctx.bezierCurveTo(this.p1.x, this.p1.y, this.p2.x, this.p2.y, this.p3.x, this.p3.y);
        ctx.stroke();
    },
    clone:function(offsetX, offsetY) {
        offsetX = offsetX || 0;
        offsetY = offsetY || 0;

        return new Curve(
            this.p0.clone(offsetX, offsetY),
            this.p1.clone(offsetX, offsetY),
            this.p2.clone(offsetX, offsetY),
            this.p3.clone(offsetX, offsetY)
        );
    }
};

Point = function(x, y) {
    this.x = x || 0;
    this.y = y || 0;
};
Point.prototype = {
    clone:function(offsetX, offsetY) {
        offsetX = offsetX || 0;
        offsetY = offsetY || 0;

        return new Point(this.x + offsetX, this.y + offsetY);
    },
    copy:function(p) {
        this.x = p.x;
        this.y = p.y;

        return this;
    },
    translate:function(dx, dy) {
        dx = dx || 0;
        dy = dy || 0;

        this.x += dx;
        this.y += dy;

        return this;
    }
};
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.