const ctx = canvas.getContext("2d");
var W = canvas.width, H = canvas.height, resized = false;
const TAU = Math.PI * 2;
const Resize = () => { W = canvas.width = innerWidth; H = canvas.height = innerHeight; resized = true; }
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX; mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["mousedown","mouseup","mousemove"].forEach(name => document.addEventListener(name,mouseEvents));
const V2 = (x = 0, y = 0) => ({
x, y,
set(v) { this.x = v.x; this.y = v.y; return this; },
sub(v) { this.x -= v.x; this.y -= v.y; return this; },
length() { return (this.x * this.x + this.y * this.y) ** 0.5; },
normalize(len = this.length()) { this.x /= len; this.y /= len; return this; },
neg() { this.x = -this.x; this.y = -this.y; return this; },
cross(v) { return this.x * v.y - this.y * v.x; },
scale(s) { this.x *= s; this.y *= s; return this; },
});
const Light = {
pos: V2(),
Update() {
if (resized) {
resized = false;
this.pos.x = W * 0.5;
this.pos.y = H * 0.5;
this.grd = ctx.createRadialGradient(this.pos.x, this.pos.y, 10, this.pos.x, this.pos.y, 1000);
this.grd.addColorStop(0, "#FFF");
this.grd.addColorStop(0.01, "#BA4");
this.grd.addColorStop(1, "#220");
}
},
Draw() {
ctx.fillStyle = this.grd;
ctx.fillRect(0, 0, W, H);
},
};
const Shape = {
pos: V2(),
col: "#034",
sCol: "#8BD",
verts: [],
tVerts: [], // transformed
leftVertIdx: -1, // -1 for unknown
rightVertIdx: -1, // -1 for unknown
Clear() {
this.verts.length = 0;
this.tVerts.length = 0;
this.leftVertIdx = -1;
this.rightVertIdx = -1;
},
Add(...verts) {
this.verts.push(...verts);
for (const v of verts) { this.tVerts.push(V2(v.x, v.y)); }
},
Update(pos, rot) { // translate and rotate shape
i = 0;
this.pos.set(pos);
const xAx = Math.cos(rot);
const xAy = Math.sin(rot);
for (const v of this.verts) {
const tV = this.tVerts[i++];
tV.x = v.x * xAx - v.y * xAy + pos.x;
tV.y = v.x * xAy + v.y * xAx + pos.y;
}
},
Draw() {
ctx.fillStyle = this.col;
ctx.strokeStyle = this.sCol;
ctx.beginPath();
for (const tV of this.tVerts) { ctx.lineTo(tV.x, tV.y); }
ctx.closePath();
ctx.fill();
ctx.stroke();
},
DrawExtrema(from) {
const wV = V2();
ctx.fillStyle = "#220";
ctx.beginPath();
if (this.leftVertIdx > -1) {
const tV = this.tVerts[this.leftVertIdx];
wV.set(tV).sub(from).normalize().scale(10000);
ctx.moveTo(tV.x + wV.x, tV.y + wV.y);
ctx.lineTo(tV.x, tV.y);
}
if (this.rightVertIdx > -1) {
let i = (this.leftVertIdx + 1) % this.verts.length;
while (i != this.rightVertIdx) {
ctx.lineTo(this.tVerts[i].x, this.tVerts[i].y);
i = (i + 1) % this.verts.length;
}
const tV = this.tVerts[this.rightVertIdx];
wV.set(tV).sub(from).normalize().scale(10000);
ctx.lineTo(tV.x, tV.y);
ctx.lineTo(tV.x + wV.x, tV.y + wV.y);
}
ctx.fill();
ctx.fillStyle = "#000";
},
DrawLineToLight(light) {
ctx.strokeStyle = "#FFF";
ctx.beginPath();
if (this.leftVertIdx > -1) {
ctx.moveTo(light.x, light.y);
ctx.lineTo(this.tVerts[this.leftVertIdx].x, this.tVerts[this.leftVertIdx].y);
}
if (this.rightVertIdx > -1) {
ctx.moveTo(light.x, light.y);
ctx.lineTo(this.tVerts[this.rightVertIdx].x, this.tVerts[this.rightVertIdx].y);
}
ctx.stroke();
},
FindExtrema(from) {
const left = V2().set(from).sub(this.pos).normalize();
const right = V2().set(from).sub(this.pos).normalize();
const wV = V2();
this.leftVertIdx = -1;
this.rightVertIdx = -1;
var i = 0, c;
for (const tV of this.tVerts) {
wV.set(tV).sub(from).normalize();
c = left.cross(wV);
if (c < 0) {
this.leftVertIdx = i;
left.set(wV).neg();
}
c = right.cross(wV);
if (c > 0) {
this.rightVertIdx = i;
right.set(wV).neg();
}
i++;
}
}
};
const RndI = (m) => Math.random() * m | 0;
const Rnd = (m, M) => Math.random() * (M-m) + m;
function RandomShape() {
Shape.Clear();
const vCount = RndI(8) + 4;
const aStep = TAU / vCount;
var ang = 0;
var i = 0;
while (i < vCount) {
const a = ang + Rnd(-0.5, 0.5) * aStep;
const dist = Rnd(50, 100);
const x = Math.cos(a) * dist;
const y = Math.sin(a) * dist;
Shape.Add(V2(x,y));
ang += aStep;
i++;
}
}
Shape.Add(V2(-50, -20), V2(50, -20), V2(50, 20), V2(-50, 20));
requestAnimationFrame(MainLoop);
function MainLoop(time) {
if (mouse.button) {
RandomShape();
mouse.button = false;
}
if (canvas.width != innerWidth || canvas.width != innerHeight) { Resize() }
ctx.clearRect(0, 0, W, H);
Light.Update();
Shape.Update(mouse, Math.sin(time * 0.0001) * 0.5);
Shape.FindExtrema(Light.pos);
Light.Draw();
Shape.DrawExtrema(Light.pos);
Shape.Draw();
Shape.DrawLineToLight(Light.pos);
requestAnimationFrame(MainLoop);
}
canvas { position: absolute; left: 0px; top: 0px; }
div { position: absolute; left: 10px; top: 10px; color: #FFF;}
<canvas id="canvas" width="10" height="10"></canvas>
<div id="info">Press mouse button to craete random shape</div>