Skip to content

Instantly share code, notes, and snippets.

@luisguillermobultetibles
Created March 15, 2023 14:44
Show Gist options
  • Save luisguillermobultetibles/3f9cd10a7fdeb2a74e1df44ead978c6f to your computer and use it in GitHub Desktop.
Save luisguillermobultetibles/3f9cd10a7fdeb2a74e1df44ead978c6f to your computer and use it in GitHub Desktop.
Lightning Points (Lightning 2)
<canvas id='c'></canvas>
/**
* requestAnimationFrame
*/
window.requestAnimationFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) { window.setTimeout(callback, 1000 / 60); };
})();
/**
* Lightning
*/
var Lightning = (function(window) {
/**
* LightningAbstract
*/
var LightningAbstract = {
points: null,
children: null,
_simplexNoise: new SimplexNoise(),
render: function(ctx, controls) {
this._update(controls);
this._draw(ctx);
},
_update: function(controls) {
throw new Error('Not override');
},
_draw: function(ctx) {
var points = this.points,
isRoot = false, opts,
p, p1, dx, dy, dist,
lineWidth,
i, len;
isRoot = !this.parent;
opts = isRoot ? this : this.parent;
if (isRoot) { // is root
var radius, gradient,
children = this.children, c;
ctx.save();
for (i = 0, len = points.length; i < len; i += len - 1) {
p = points[i];
radius = Math.random() * (8 - 3) + 3;
gradient = ctx.createRadialGradient(p.x, p.y, radius / 3, p.x, p.y, radius);
gradient.addColorStop(0, this._colorToString(1));
gradient.addColorStop(1, this._colorToString(0));
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2, false);
ctx.fill();
}
ctx.restore();
for (i = 0, len = children.length; i < len; i += len - 1) {
children[i].render(ctx);
}
}
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.lineCap = 'round';
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
ctx.shadowBlur = opts.blur;
ctx.shadowColor = this._colorToString(1);
ctx.beginPath();
for (i = 0, len = points.length; i < len; i++) {
p = points[i];
if (len > 1) {
p1 = points[i === len - 1 ? i - 1 : i + 1];
dx = p.x - p1.x;
dy = p.y - p1.y;
dist = Math.sqrt(dx * dx + dy * dy);
} else {
dist = 0;
}
if (dist > 30) dist = 30;
ctx.moveTo(p.x + dist, p.y);
ctx.arc(p.x, p.y, dist, 0, Math.PI * 2, false);
}
ctx.fill();
ctx.restore();
ctx.save();
ctx.beginPath();
ctx.strokeStyle = this._colorToString(Math.random() * (opts.maxAlpha - opts.minAlpha) + opts.minAlpha);
lineWidth = Math.random() * (opts.maxLineWidth - opts.minLineWidth) + opts.minLineWidth;
ctx.lineWidth = isRoot ? lineWidth : lineWidth * 0.5;
for (i = 0; i < len; i++) {
p = points[i];
ctx[i === 0 ? 'moveTo' : 'lineTo'](p.x, p.y);
}
ctx.stroke();
ctx.restore();
},
_noise2d: function(x, y) {
var octaves = 3,
fallout = 0.5,
amp = 1, f = 1, sum = 0,
i;
for (i = 0; i < octaves; ++i) {
amp *= fallout;
sum += amp * (this._simplexNoise.noise2D(x * f, y * f) + 1) * 0.5;
f *= 2;
}
return sum;
},
_filterApply: function(points, lineLength, segmentsNum, base, amp, offset) {
var pointsOld = this.points,
// spline
spline = [],
catmullRom = this._catmullRom,
p0, p1, p2, p3, t, per,
// noise
p, next, angle, sin, cos, nx, av, ax, ay, bv, bx, by, m, px, py,
// shortest
shortest, dx, dy, distSq, minDist,
i, len, j, k;
// Spline
// スプライン補完用に配列の前後にラインの始点, 終点の参照をそれぞれ複製する
points.unshift(points[0]);
points.push(points[points.length - 1]);
per = 1 / segmentsNum;
// スプライン曲線のポイントを取得
for (i = 0, len = points.length - 3; i < len; i++) {
p0 = points[i];
p1 = points[i + 1];
p2 = points[i + 2];
p3 = points[i + 3];
for (j = 0; j < segmentsNum; j++) {
t = (j + 1) * per;
spline.push({
x: catmullRom(p0.x, p1.x, p2.x, p3.x, t),
y: catmullRom(p0.y, p1.y, p2.y, p3.y, t)
});
}
}
// 補完用に追加した参照を削除
points.pop();
// 削除のついでに描画の始点として追加
spline.unshift(points.shift());
// Noise
points = [];
len = spline.length;
per = 1 / (len - 1);
base = 1 / base;
for (i = 0, len = spline.length; i < len; i++) {
p = spline[i];
next = i === len - 1 ? p : spline[i + 1];
angle = Math.atan2(next.y - p.y, next.x - p.x);
sin = Math.sin(angle);
cos = Math.cos(angle);
nx = i * base;
av = lineLength * this._noise2d(nx - offset, offset) * 0.5 * amp;
ax = av * sin;
ay = av * cos;
bv = lineLength * this._noise2d(nx + offset, offset) * 0.5 * amp;
bx = bv * sin;
by = bv * cos;
m = Math.sin(Math.PI * (i * per));
px = p.x + (ax - bx) * m;
py = p.y - (ay - by) * m;
if (pointsOld.length) {
p = pointsOld.shift();
p.x = px;
p.y = py;
} else {
p = { x: px, y: py };
}
points.push(p);
}
// Shortest
shortest = [points[0]];
for (i = 0, len = points.length; i < len; i++) {
p = points[i];
minDist = Infinity;
k = -1;
for (j = i; j < len; j++) {
p2 = points[j];
dx = p.x - p2.x;
dy = p.y - p2.y;
distSq = dx * dx + dy * dy;
if (p !== p2 && distSq < minDist * minDist) {
minDist = Math.sqrt(distSq);
k = j;
}
}
if (k < 0) break;
shortest.push(points[k]);
i = k - 1;
}
return shortest;
},
_catmullRom: function(p0, p1, p2, p3, t) {
var v0 = (p2 - p0) * 0.5,
v1 = (p3 - p1) * 0.5;
return (2 * p1 - 2 * p2 + v0 + v1) * t * t * t +
(-3 * p1 + 3 * p2 - 2 * v0 - v1) * t * t + v0 * t + p1;
},
_colorToString: function(alpha) {
var c = this.color;
return this.colorType === 'rgb' ?
'rgba(' + c.join(',') + ',' + alpha + ')' :
'hsla(' + c[0] + ',' + c[1] + '%,' + c[2] + '%,' + alpha + ')';
}
};
/**
* @constructor
*/
function Lightning(segmentsNum) {
this.points = [];
this.children = [];
this._params = [];
this._offsets = [];
}
Lightning.prototype = extend(LightningAbstract, {
color: [255, 255, 255],
colorType: 'rgb',
blur: 50,
maxAlpha: 1,
minAlpha: 0.75,
maxLineWidth: 5,
minLineWidth: 0.5,
_params: null,
_offsets: null,
addParam: function(segmentsNum, base, amplitude, speed) {
this._params.push({
segmentsNum: segmentsNum,
base: base,
amplitude: amplitude,
speed: speed
});
this._offsets[this._params.length - 1] = 0;
},
createChild: function(base, amplitude, speed) {
var child = new LChild(this, {
base: base || this._params.base,
amplitude: amplitude || this._params.amplitude,
speed: speed || this._params.speed
});
this.children.push(child);
return child;
},
_update: function(points) {
var params = this._params, param,
offsets = this._offsets,
lineLength,
p0, p1, dx, dy,
i, ilen, j, jlen;
for (i = 0, ilen = params.length; i < ilen; i++) {
param = params[i];
lineLength = 0;
for (j = 0, jlen = points.length; j < jlen; j++) {
if (j !== jlen - 1) {
p0 = points[j];
p1 = points[j + 1];
dx = p0.x - p1.x;
dy = p0.y - p1.y;
lineLength += dx * dx + dy * dy;
}
}
lineLength = Math.sqrt(lineLength);
offsets[i] += Math.random() * param.speed;
points = this._filterApply(points, lineLength, param.segmentsNum, param.base, param.amplitude, offsets[i]);
}
this.points = points;
}
});
/**
* LChild
*/
function LChild(parent, param) {
this.parent = parent;
this.points = [];
this._param = param;
}
LChild.prototype = extend(LightningAbstract, {
parent: null,
_startStep: 0,
_endStep: 0,
_separate: 2,
_param: null,
_offset: 0,
_lastChangeTime: 0,
_update: function() {
var parent = this.parent,
plen = this.parent.points.length,
param = this._param,
points = [],
currentTime,
range, rangeLen, sep, seg,
c0, c1, dx, dy, lineLength,
i, j;
// 一定時間ごと, あるいは親のポイントの数が子の終了ステップ位置を下回った場合に始点と終点の親からの取得位置を更新する
currentTime = new Date().getTime();
if (
currentTime - this._lastChangeTime > 10000 * Math.random() ||
plen < this._endStep
) {
var stepMin = plen * 0.1 | 0,
startStep = this._startStep = (Math.random() * (plen / 3 * 2 | 0) | 0);
this._endStep = startStep + stepMin + ((Math.random() * (plen - startStep - stepMin) + 1) | 0);
this._lastChangeTime = currentTime;
}
// 親のポイント配列から取得範囲を切り出す
range = parent.points.slice(this._startStep, this._endStep);
rangeLen = range.length;
// 範囲からスプライン曲線の制御点を取得する
seg = (rangeLen - 1) / this._separate;
for (i = 0; i <= this._separate; i++) {
j = seg * i | 0;
points.push(range[j]);
}
// ノイズの実際の振り幅
c0 = points[0];
c1 = points[points.length - 1];
dx = c0.x - c1.x;
dy = c0.y - c1.y;
lineLength = Math.sqrt(dx * dx + dy * dy);
this._offset += Math.random() * param.speed;
this.points = this._filterApply(points, lineLength, rangeLen * 0.5 | 0, param.base, param.amplitude, this._offset);
},
_colorToString: function(alpha) {
var c = this.parent.color;
return this.parent.colorType === 'rgb' ?
'rgba(' + c.join(',') + ',' + alpha + ')' :
'hsla(' + c[0] + ',' + c[1] + '%,' + c[2] + '%,' + alpha + ')';
}
});
// Helpers
function extend() {
var t = {}, o, p, i, len;
for (i = 0, len = arguments.length; i < len; i++) {
o = arguments[i];
for (p in o) t[p] = o[p];
}
return t;
}
return Lightning;
})(window);
/**
* Point
*/
function Point(x, y, color, colorType) {
this.x = x;
this.y = y;
this.color = color;
this.colorType = colorType;
this.vx = Math.random() * (3 + 3) - 3;
this.vy = Math.random() * (3 + 3) - 3;
this._latest = { x: this.x, y: this.y };
}
Point._field = null;
Point.setField = function(x, y, width, height) {
Point._field = {
x: x,
y: y,
width: width,
height: height,
right: x + width,
bottom: y + height
};
};
Point.prototype = {
color: null,
colorType: 'rgb',
radius: 50,
alpha: 0.2,
dragging: false,
dying: false,
dead: false,
_mouse: null,
_latest: null,
_mouseDist: null,
_currentAlpha: 0,
_currentRadius: 0,
lengthSq: function() {
return this.x * this.x + this.y * this.y;
},
hitTest: function(mouse) {
var dx = mouse.x - this.x,
dy = mouse.y - this.y;
return dx * dx + dy * dy < this.radius * this.radius;
},
dragStart: function(mouse) {
if (this.hitTest(mouse)) {
this._mouse = mouse;
this._mouseDist = {
x: this.x - mouse.x,
y: this.y - mouse.y
};
this.dragging = true;
}
return this.dragging;
},
dragEnd: function() {
this.dragging = false;
this._mouse = this._mouseDist = null;
},
kill: function() {
this.dying = true;
this.radius = 0;
},
update: function(mouse) {
var field = Point._field,
radius = this.radius,
vlen, d;
if (this._mouse) {
this._latest.x = this.x;
this._latest.y = this.y;
this.x = this._mouse.x + this._mouseDist.x;
this.y = this._mouse.y + this._mouseDist.y;
this.vx = this.x - this._latest.x;
this.vy = this.y - this._latest.y;
} else {
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.98;
this.vy *= 0.98;
}
vlen = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
if (vlen && vlen > 20) {
this.vx /= vlen / 20;
this.vy /= vlen / 20;
}
if (this.x < field.x + radius) {
this.x = field.x + radius;
if (this.vx < 0) this.vx *= -1;
} else if (this.x > field.right - radius) {
this.x = field.right - radius;
if (this.vx > 0) this.vx *= -1;
}
if (this.y < field.y + radius) {
this.y = field.y + radius;
if (this.vy < 0) this.vy *= -1;
} else if (this.y > field.bottom - radius) {
this.y = field.bottom - radius;
if (this.vy > 0) this.vy *= -1;
}
// 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 *= Math.random() * (1 - 0.85) + 0.85;
},
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, this._colorToString(this._currentAlpha));
gradient.addColorStop(1, this._colorToString(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();
},
_colorToString: function(alpha) {
var c = this.color;
return this.colorType === 'rgb' ?
'rgba(' + c.join(',') + ',' + alpha + ')' :
'hsla(' + c[0] + ',' + c[1] + '%,' + c[2] + '%,' + alpha + ')';
}
};
(function() {
// Configs
var DRAG_POINT_NUM = 4,
DRAG_POINT_MAX_NUM = 8,
CHILD_NUM = 2,
LIHTNING_COLOR = [195, 100, 50]; // HSL
BACKGROUND_COLOR = 'rgba(0, 15, 20, 0.8)';
// Vars
var canvas, context,
canvasMinSize, centerX, centerY,
points = [],
mouse = { x: 0, y: 0 },
lightning,
grad,
i;
// Event Listeners
function resize(e) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
context = canvas.getContext('2d');
context.lineCap = 'round';
canvasMinSize = Math.min(canvas.width, canvas.height);
centerX = canvas.width * 0.5;
centerY = canvas.height * 0.5;
grad = context.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, 'hsla(195, 100%, 50%, 0.08)');
grad.addColorStop(0.5, 'hsla(195, 100%, 50%, 0)');
grad.addColorStop(1, 'hsla(195, 100%, 50%, 0.08)');
Point.setField(0, 0, canvas.width, canvas.height);
}
function mouseMove(e) {
mouse.x = e.clientX;
mouse.y = e.clientY;
var hit = false, i, len;
for (i = 0, len = points.length; i < len; i++) {
if (points[i].hitTest(mouse)) {
hit = true;
break;
}
}
document.body.style.cursor = hit ? 'pointer' : 'default';
}
function mouseDown(e) {
var i, len;
for (i = 0, len = points.length; i < len; i++) {
if (points[i].dragStart(mouse)) return;
}
for (i = 0; i < len; i++) {
if (points[i].hitTest(mouse)) {
if (len > 1) points.splice(i, 1);
return;
}
}
if (len < DRAG_POINT_MAX_NUM) {
points.push(createPoint(e.clientX, e.clientY));
} else {
for (i = 0; i < len - 2; i++) {
points[i].kill();
}
}
}
function mouseUp(e) {
for (var i = 0, len = points.length; i < len; i++) {
points[i].dragEnd(mouse);
}
}
function doubleClick(e) {
var i, len = points.length;
if (len < 3) return;
for (i = 0; i < len; i++) {
if (points[i].hitTest(mouse)) {
points[i].kill();
return;
}
}
}
// Functions
function createPoint(x, y) {
return new Point(x, y, LIHTNING_COLOR.slice(), 'hsl');
}
// Array sort callback
function sortPoints(p1, p2) {
return p1.lengthSq() - p2.lengthSq();
}
// Init
document.body.style.backgroundColor = BACKGROUND_COLOR;
canvas = document.getElementById('c');
window.addEventListener('resize', resize, false);
resize(null);
for (i = 0; i < DRAG_POINT_NUM; i++) {
points.push(createPoint(Math.random() * canvasMinSize + centerX - canvasMinSize * 0.5, Math.random() * canvasMinSize + centerY - canvasMinSize * 0.5));
}
lightning = new Lightning();
lightning.addParam(8, 10, 0.7, 0.01); // segumentsNum, noiseBase, amplitude, speed
lightning.addParam(16, 60, 0.5, 0.03);
lightning.colorType = 'hsl';
lightning.color = LIHTNING_COLOR.slice();
for (i = 0; i < CHILD_NUM; i++) {
lightning.createChild(80, 0.5, 0.06); // noiseBase, amplitude, speed
}
canvas.addEventListener('mousemove', mouseMove, false);
canvas.addEventListener('mousedown', mouseDown, false);
canvas.addEventListener('mouseup', mouseUp, false);
canvas.addEventListener('dblclick', doubleClick, false);
// Start update
var loop = function() {
var controls = [],
i, len, p;
context.save();
context.globalCompositeOperation = 'source-over';
context.fillStyle = BACKGROUND_COLOR;
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = grad;
context.fillRect(0, 0, canvas.width, canvas.height);
context.restore();
context.globalCompositeOperation = 'lighter';
for (i = 0, len = points.length; i < len; i++) {
p = points[i];
p.update();
p.alpha = p.hitTest(mouse) ? 0.75 : 0.2;
p.draw(context);
if (p.dead) {
points.splice(i, 1);
i--;
len--;
continue;
}
if (!p.dying) controls.push(p);
}
// 原点からの距離でソート
controls.sort(sortPoints);
lightning.render(context, controls);
lightning.color[2] = Math.random() * (100 - 35) + 35;
requestAnimationFrame(loop);
};
loop();
})();
<script src="https://rawgit.com/akm2/simplex-noise.js/master/simplex-noise.js"></script>
body {
font-family: Helvetica sans-serif;
padding: 0;
margin: 0;
background-color: #222;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-o-user-select: none;
-ms-user-select: none;
user-select: none;
}
canvas {
position: absolute;
top: 0;
left: 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment