Can anyone give me a hint how I can create something like that with javascript:
The requirement is that I can set the density of the flakes. and add up to 5 different colors.
I do know how to create a canvas and put pixels in there, but I don't know how to create the "flakes".
Is there a way to create random shapes like this?
You can tessellate a simple shape and draw it at some random point.
The example below will create a 3 sided point, testate it randomly to a detail level of about 2 pixels and then add it to a path.
Then the path is filled with a color and another set of shapes are added.
function testate(amp, points) {
const p = [];
var i = points.length - 2, x1, y1, x2, y2;
p.push(x1 = points[i++]);
p.push(y1 = points[i]);
i = 0;
while (i < points.length) {
x2 = points[i++];
y2 = points[i++];
const dx = x2 - x1;
const dy = y2 - y1;
const r = (Math.random() - 0.5) * 2 * amp;
p.push(x1 + dx / 2 - dy * r);
p.push(y1 + dy / 2 + dx * r);
p.push(x1 = x2);
p.push(y1 = y2);
}
return p;
}
function drawFlake(ctx, size, x, y, noise) {
const a = Math.random() * Math.PI;
var points = [];
const step = Math.PI * (2/3);
var i = 0;
while (i < 3) {
const r = (Math.random() * size + size) / 2;
points.push(Math.cos(a + i * step) * r);
points.push(Math.sin(a + i * step) * r);
i++;
}
while (size > 2) {
points = testate(noise, points);
size >>= 1;
}
i = 0;
ctx.setTransform(1,0,0,1,x,y);
ctx.moveTo(points[i++], points[i++]);
while (i < points.length) {
ctx.lineTo(points[i++], points[i++]);
}
}
function drawRandomFlakes(ctx, count, col, min, max, noise) {
ctx.fillStyle = col;
ctx.beginPath();
while (count-- > 0) {
const x = Math.random() * ctx.canvas.width;
const y = Math.random() * ctx.canvas.height;
const size = min + Math.random() * (max- min);
drawFlake(ctx, size, x, y, noise);
}
ctx.fill();
}
const ctx = canvas.getContext("2d");
canvas.addEventListener("click",drawFlakes);
drawFlakes();
function drawFlakes(){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle = "#341";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height)
const noise = Math.random() * 0.3 + 0.3;
drawRandomFlakes(ctx, 500, "#572", 5, 10, noise)
drawRandomFlakes(ctx, 200, "#421", 10, 15, noise)
drawRandomFlakes(ctx, 25, "#257", 15, 30, noise)
}
body { background: #341 }
div {
position: absolute;
top: 20px;
left: 20px;
color: white;
}
<canvas id="canvas" width = "600" height = "512"></canvas>
<div>Click to redraw</div>
You'll need a certain noise algorithm.
In this example I used Perlin noise, but you can use any noise algorithm that fits your needs. By using Perlin noise we can define a blob as an area where the noise value is above a certain threshold.
I used a library that I found here and based my code on the sample code. The minified code is just a small portion of it (I cut out simplex and perlin 3D).
LICENSE
You can tweek it by changing the following parameters
Math.abs(noise.perlin2(x / 25, y / 25))
Changing the 25 to a higher value will zoom in, lower will zoom out
if (value > 0.4){
Changing the 0.4 to a lower value will increase blob size, higher will decrease blob size.
!function(n){var t=n.noise={};function e(n,t,e){this.x=n,this.y=t,this.z=e}e.prototype.dot2=function(n,t){return this.x*n+this.y*t},e.prototype.dot3=function(n,t,e){return this.x*n+this.y*t+this.z*e};var r=[new e(1,1,0),new e(-1,1,0),new e(1,-1,0),new e(-1,-1,0),new e(1,0,1),new e(-1,0,1),new e(1,0,-1),new e(-1,0,-1),new e(0,1,1),new e(0,-1,1),new e(0,1,-1),new e(0,-1,-1)],o=[151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180],i=new Array(512),w=new Array(512);function u(n){return n*n*n*(n*(6*n-15)+10)}function f(n,t,e){return(1-e)*n+e*t}t.seed=function(n){n>0&&n<1&&(n*=65536),(n=Math.floor(n))<256&&(n|=n<<8);for(var t=0;t<256;t++){var e;e=1&t?o[t]^255&n:o[t]^n>>8&255,i[t]=i[t+256]=e,w[t]=w[t+256]=r[e%12]}},t.seed(0),t.perlin2=function(n,t){var e=Math.floor(n),r=Math.floor(t);n-=e,t-=r;var o=w[(e&=255)+i[r&=255]].dot2(n,t),h=w[e+i[r+1]].dot2(n,t-1),s=w[e+1+i[r]].dot2(n-1,t),a=w[e+1+i[r+1]].dot2(n-1,t-1),c=u(n);return f(f(o,s,c),f(h,a,c),u(t))}}(this);
const c = document.getElementById("canvas");
const cc = c.getContext("2d");
noise.seed(Math.random());
let image = cc.createImageData(canvas.width, canvas.height);
let data = image.data;
for (let x = 0; x < c.width; x++){
for (let y = 0; y < c.height; y++){
const value = Math.abs(noise.perlin2(x / 25, y / 25));
const cell = (x + y * c.width) * 4;
if (value > 0.4){
data[cell] = 256;
data[cell + 1] = 0;
data[cell + 2] = 0;
data[cell + 3] = 256;
}
else {
data[cell] = 0;
data[cell + 1] = 0;
data[cell + 2] = 0;
data[cell + 3] = 0;
}
}
}
cc.putImageData(image, 0, 0);
<canvas id="canvas" width=500 height=500></canvas>
Related
In the Below code link HTML5 canvas spin wheel game. I want to stop this canvas at a user-defined position as if the user wants to stop always at 200 texts or 100 texts like that.
Currently, it is stopping at random points I want to control where to stop as in if I want to stop circle at 100 or 200 or 0 whenever I want.
How can we achieve that??? Can anyone Help!!!!!
Attached Codepen link also.
Html file
<div>
<canvas class="spin-wheel" id="canvas" width="300" height="300"></canvas>
</div>
JS file
var color = ['#ca7','#7ac','#77c','#aac','#a7c','#ac7', "#caa"];
var label = ['10', '200','50','100','5','500',"0"];
var slices = color.length;
var sliceDeg = 360/slices;
var deg = 270;
var speed = 5;
var slowDownRand = 0;
var ctx = canvas.getContext('2d');
var width = canvas.width; // size
var center = width/2; // center
var isStopped = false;
var lock = false;
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function deg2rad(deg){ return deg * Math.PI/180; }
function drawSlice(deg, color){
ctx.beginPath();
ctx.fillStyle = color;
ctx.moveTo(center, center);
ctx.arc(center, center, width/2, deg2rad(deg), deg2rad(deg+sliceDeg));
console.log(center, center, width/2, deg2rad(deg), deg2rad(deg+sliceDeg))
ctx.lineTo(center, center);
ctx.fill();
}
function drawText(deg, text) {
ctx.save();
ctx.translate(center, center);
ctx.rotate(deg2rad(deg));
ctx.textAlign = "right";
ctx.fillStyle = "#fff";
ctx.font = 'bold 30px sans-serif';
ctx.fillText(text, 130, 10);
ctx.restore();
}
function drawImg() {
ctx.clearRect(0, 0, width, width);
for(var i=0; i<slices; i++){
drawSlice(deg, color[i]);
drawText(deg+sliceDeg/2, label[i]);
deg += sliceDeg;
}
}
// ctx.rotate(360);
function anim() {
isStopped = true;
deg += speed;
deg %= 360;
// Increment speed
if(!isStopped && speed<3){
speed = speed+1 * 0.1;
}
// Decrement Speed
if(isStopped){
if(!lock){
lock = true;
slowDownRand = rand(0.994, 0.998);
}
speed = speed>0.2 ? speed*=slowDownRand : 0;
}
// Stopped!
if(lock && !speed){
var ai = Math.floor(((360 - deg - 90) % 360) / sliceDeg); // deg 2 Array Index
console.log(slices)
ai = (slices+ai)%slices; // Fix negative index
return alert("You got:\n"+ label[ai] ); // Get Array Item from end Degree
// ctx.arc(150,150,150,8.302780584487312,9.200378485512967);
// ctx.fill();
}
drawImg();
window.requestAnimationFrame(anim);
}
function start() {
anim()
}
drawImg();
Spin wheel codepen
Ease curves
If you where to plot the wheel position over time as it slows to a stop you would see a curve, a curve that looks like half a parabola.
You can get the very same curve if you plot the value of x squared in the range 0 to 1 as in the next snippet, the red line shows the plot of f(x) => x * x where 0 <= x <= 1
Unfortunately the plot is the wrong way round and needs to be mirrored in x and y. That is simple by changing the function to f(x) => 1 - (1 - x) ** 2 (Click the canvas to get the yellow line)
const size = 200;
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size / 2}).getContext("2d");
document.body.appendChild(ctx.canvas);
ctx.canvas.style.border = "2px solid black";
plot(getData());
plot(unitCurve(x => x * x), "#F00");
ctx.canvas.addEventListener("click",()=>plot(unitCurve(x => 1 - (1 - x) ** 2), "#FF0"), {once: true});
function getData(chart = []) {
var pos = 0, speed = 9, deceleration = 0.1;
while(speed > 0) {
chart.push(pos);
pos += speed;
speed -= deceleration;
}
return chart;
}
function unitCurve(f,chart = []) {
const step = 1 / 100;
var x = 0;
while(x <= 1) {
chart.push(f(x));
x += step
}
return chart;
}
function plot(chart, col = "#000") {
const xScale = size / chart.length, yScale = size / 2 / Math.max(...chart);
ctx.setTransform(xScale, 0, 0, yScale, 0, 0);
ctx.strokeStyle = col;
ctx.beginPath();
chart.forEach((y,x) => ctx.lineTo(x,y));
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
In animation this curve is an ease in.
We can create function that uses the ease function, takes the time and returns the position of the wheel. We can provide some additional values that controls how long the wheel will take to stop, the starting position and the all important stop position.
function wheelPos(currentTime, startTime, endTime, startPos, endPos) {
// first scale the current time to a value from 0 to 1
const x = (currentTime - startTime) / (endTime - startTime);
// rather than the square, we will use the square root (this flips the curve)
const xx = x ** (1 / 2);
// convert the value to a wheel position
return xx * (endPos - startPos) + startPos;
}
Demo
The demo puts it in action. Rather than using the square root the function in the demo defines the root as the constant slowDownRate = 2.6. The smaller this value the greater start speed and the slower the end speed. A value of 1 means it will move at a constant speed and then stop. The value must be > 0 and < 1
requestAnimationFrame(mainLoop);
Math.TAU = Math.PI * 2;
const size = 160;
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size}).getContext("2d");
document.body.appendChild(ctx.canvas);
const stopAt = document.createElement("div")
document.body.appendChild(stopAt);
ctx.canvas.style.border = "2px solid black";
var gTime; // global time
const colors = ["#F00","#F80","#FF0","#0C0","#08F","#00F","#F0F"];
const wheelSteps = 12;
const minSpins = 3 * Math.TAU; // min number of spins before stopping
const spinTime = 6000; // in ms
const slowDownRate = 1 / 1.8; // smaller this value the greater the ease in.
// Must be > 0
var startSpin = false;
var readyTime = 0;
ctx.canvas.addEventListener("click",() => { startSpin = !wheel.spinning });
stopAt.textContent = "Click wheel to spin";
const wheel = { // hold wheel related variables
img: createWheel(wheelSteps),
endTime: performance.now() - 2000,
startPos: 0,
endPos: 0,
speed: 0,
pos: 0,
spinning: false,
set currentPos(val) {
this.speed = (val - this.pos) / 2; // for the wobble at stop
this.pos = val;
},
set endAt(pos) {
this.endPos = (Math.TAU - (pos / wheelSteps) * Math.TAU) + minSpins;
this.endTime = gTime + spinTime;
this.startTime = gTime;
stopAt.textContent = "Spin to: "+(pos + 1);
}
};
function wheelPos(currentTime, startTime, endTime, startPos, endPos) {
const x = ((currentTime - startTime) / (endTime - startTime)) ** slowDownRate;
return x * (endPos - startPos) + startPos;
}
function mainLoop(time) {
gTime = time;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0, 0, size, size);
if (startSpin && !wheel.spinning) {
startSpin = false;
wheel.spinning = true;
wheel.startPos = (wheel.pos % Math.TAU + Math.TAU) % Math.TAU;
wheel.endAt = Math.random() * wheelSteps | 0;
} else if (gTime <= wheel.endTime) { // wheel is spinning get pos
wheel.currentPos = wheelPos(gTime, wheel.startTime, wheel.endTime, wheel.startPos, wheel.endPos);
readyTime = gTime + 1500;
} else { // wobble at stop
wheel.speed += (wheel.endPos - wheel.pos) * 0.0125;
wheel.speed *= 0.95;
wheel.pos += wheel.speed;
if (wheel.spinning && gTime > readyTime) {
wheel.spinning = false;
stopAt.textContent = "Click wheel to spin";
}
}
// draw wheel
ctx.setTransform(1,0,0,1,size / 2, size / 2);
ctx.rotate(wheel.pos);
ctx.drawImage(wheel.img, -size / 2 , - size / 2);
// draw marker shadow
ctx.setTransform(1,0,0,1,1,4);
ctx.fillStyle = "#0004";
ctx.beginPath();
ctx.lineTo(size - 13, size / 2);
ctx.lineTo(size, size / 2 - 7);
ctx.lineTo(size, size / 2 + 7);
ctx.fill();
// draw marker
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle = "#F00";
ctx.beginPath();
ctx.lineTo(size - 13, size / 2);
ctx.lineTo(size, size / 2 - 7);
ctx.lineTo(size, size / 2 + 7);
ctx.fill();
requestAnimationFrame(mainLoop);
}
function createWheel(steps) {
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size}).getContext("2d");
const s = size, s2 = s / 2, r = s2 - 4;
var colIdx = 0;
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const aa = a - Math.PI / steps;
ctx.fillStyle = colors[colIdx++ % colors.length];
ctx.beginPath();
ctx.moveTo(s2, s2);
ctx.arc(s2, s2, r, aa, aa + Math.TAU / steps);
ctx.fill();
}
ctx.fillStyle = "#FFF";
ctx.beginPath();
ctx.arc(s2, s2, 12, 0, Math.TAU);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 2;
ctx.arc(s2, s2, r, 0, Math.TAU);
ctx.moveTo(s2 + 12, s2);
ctx.arc(s2, s2, 12, 0, Math.TAU);
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const aa = a - Math.PI / steps;
ctx.moveTo(Math.cos(aa) * 12 + s2, Math.sin(aa) * 12 + s2);
ctx.lineTo(Math.cos(aa) * r + s2, Math.sin(aa) * r + s2);
}
//ctx.fill("evenodd");
ctx.stroke();
ctx.fillStyle = "#000";
ctx.font = "13px arial black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const tr = r - 8;
var idx = 1;
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const dx = Math.cos(a);
const dy = Math.sin(a);
ctx.setTransform(dy, -dx, dx, dy, dx * (tr - 4) + s2, dy * (tr - 4) + s2);
ctx.fillText(""+ (idx ++), 0, 0);
}
return ctx.canvas;
}
body { font-family: arial }
I have some js code that i copied from a youtube tutorial and adapted for my own project to fill the header, the code works as intended and it works when the viewport is smaller than around 1200px, however when i put firefox into full screen the animation does not play & the image is being stretched, not retaining its aspect ratio. I do have a 10/15 year old gpu so i'm guessing thats half my issue. The script uses a png image file of 100x100 pixels, which it then converts into particle color values. Can this be optimized or made to run better. it seems that the wider the viewport the longer the animation takes to kick in, until it finally stops & doesn't work. full screen= [2550x1440]...
The original tutorial is here: Pure Javascript Particle Animations & to convert an image to base64 encoding is here: Image to base64.
HTML:
<html>
<body>
<canvas id="CanV"></canvas>
</body>
</html>
CSS:
#Canv{
position:absolute;
top:-1px;left:-2px;
z-index:67;
width:100vw !important;
max-height: 264px !important;
min-height: 245px !important;
filter:blur(2.27px);
}
Javascript:
window.addEventListener("DOMContentLoaded",(e)=>{
const canv = document.getElementById('Canv');
const ctx = canv.getContext('2d');
canv.width = window.innerWidth;
canv.height = window.innerHeight/ 3.85;
let particleArray = [];
let mouse = {
x: null,
y: null,
radius: 74
}
window.addEventListener('mousemove',(e)=>{
mouse.x = event.x + canv.clientLeft/2;
mouse.y = event.y + canv.clientTop/1.2;
});
function drawImage(){
let imageWidth = png.width; //These to values crop if / sum no.
let imageHeight = png.height;
const data = ctx.getImageData(0, 0, imageWidth, imageHeight); //Gets img data for particles
ctx.clearRect(0,0, canv.width, canv.height); // Clears the original img as its now being stored in the variable data.
class Particle {
constructor(x, y, color, size){
this.x = x + canv.width/2 - png.width * 174, //Chngd Ok:74
this.y = y + canv.height/2 - png.height * 32, //Ch<2 Ok:16
this.color = color,
this.size = 2.28, // Particle Size > Changed this value. from 2 i think!.
this.baseX = x + canv.width/1.8 - png.width * 3.1, //Chngd ok:5.1
this.baseY = y + canv.height/1.2 - png.height * 2.8,
this.density = (Math.random() * 14) + 2;
}
draw() {
ctx.beginPath(); // this creates the sort of force field around the mouse pointer.
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
update() {
ctx.fillStyle = this.color;
// Collision detection
let dx = mouse.x - this.x;
let dy = mouse.y - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
let forceDirectionX = dx / distance;
let forceDirectionY = dy / distance;
// Max distance, past that the force will be 0
const maxDistance = 144;
let force = (maxDistance - distance) / maxDistance;
if (force < 0) force = 0;
let directionX = (forceDirectionX * force * this.density * 0.6);
let directionY = (forceDirectionY * force * this.density * 8.7); //Ch.this
if (distance < mouse.radius + this.size) {
this.x -= directionX;
this.y -= directionY;
} else {
if (this.x !== this.baseX){
let dx = this.x - this.baseX;
this.x -= dx/54; // Speed Particles return to ori
} if (this.y !== this.baseY){
let dy = this.y - this.baseY;
this.y -= dy/17; // Speed Particles return to ori
}
}
this.draw();
}
}
function init(){
particleArray = [];
for(let y = 0, y2 = data.height; y<y2; y++){
for(let x =0, x2 = data.width; x<x2; x++){
if(data.data[(y * 4 * data.width) + (x*4) + 3] > 128){
let positionX = x + 25;
let positionY = y + 45; // Co-ords on Canv
let color = "rgb(" + data.data[(y * 4 * data.width) + (x * 4)] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 1] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 2] + ")";
particleArray.push(new Particle(positionX * 2, positionY * 2, color));
} /* These number effect png size but its to high */
}
}
}
function animate(){
requestAnimationFrame(animate);
ctx.fillStyle = 'rgba(0,0,0,.07)';
ctx.fillRect(0,0, innerWidth, innerHeight);
for(let i =0; i < particleArray.length; i++){
particleArray[i].update();
}
}
init();
animate();
}
const png = new Image();
png.src = "RemovedBase64StringToBig";
window.addEventListener('load',(e)=>{
console.log('page has loaded');
ctx.drawImage(png, 0, 0);
drawImage();
})
});
have managed to shorten it by about 100 characters by shortening all the variable names > PartArr, ImgWidth, DirX, DirY etc, but apart from minifying it is there any other ways to optimize this? and fix the full screen issue?
I tried to add it to a JSfiddle, So I could link to it here, but I don't think this is allowing the base64 string, its not loading anything anyway. The canvas loads, with the bg just no image or animation.
I've found out what part of the problem is with full screen, the cursor position is actually about 300px to the right of where the actual cursor is, but I still have no idea how to fix this or fix the major lagging performance issues. Guessing its alot to compute even just with 100x100.
One option I can think of to make this perform better would be to move it & its calculations, into its own dedicated web worker & convert the image to Webp but i'm still not very clued up about web workers or how to implement them properly.. Will play around & see what I can put together using All the suggestions in the comments & answers.
I'm adding these links only for future reference, when I come back to this later on:
MDN Canvas Optimizations
Html5Rocks Canvas Performance
Stack Question. Canv ~ Opti
Creating A blob From A Base 64 String in Js
Secondary bonus Question,
is there a maximum file size or max px dimensions,
that can be base64 encoded? only asking this as someone on facebook has recently sent me a question regarding another project with multiple base64 encoded images and I was unsure of the answer..
Shortening your code doesn't help much with performance. I'm using Firefox. To check what's taking your time up the most during browser runs in Firefox, you can read Performance from MDN.
The problem with your solution is your fps is dropping hard. This happens because you are painting each Particle every frame. Imagine how laggy it will be when there are thousands of Particles that you need to paint every frame. This paint call is called from your function Particle.draw (which calls the following: ctx.beginPath, ctx.arc, and ctx.closePath). This function, as said, will be called because of Particle.update for each frame. This is an extremely expensive operation. To improve your fps drastically, you can try to not draw each Particle individually, but rather gather all the Particles' ImageData wholly then placing it in the canvas only once in rAQ (thus only one paint happens). This ImageData is an object that contains the rgba for each pixel on canvas.
In my solution below, I did the following:
For each Particle that is dirty (has been updated), modify the ImageData that is to be put in the canvas
Then, after the whole ImageData has been constructed for one frame, only draw once to the canvas using putImageData. This saves a lot of the time needed to call your function Particle.update to draw each Particle individually.
One other obvious solution is to increase the size of Particles so that there are fewer Particles' pixels that are needed to be processed (to alter ImageData). I've also tweaked the code a little so that the image will always be at least 100px high; you can tweak the maths so that the image will always maintain your aspect ratio and respond to window size.
Here's a working example:
const canvas = document.querySelector('#canvas1')
const ctx = canvas.getContext('2d')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
let canvasWidth = canvas.width
let canvasHeight = canvas.height
let particleArray = []
let imageData = []
// mouse
let mouse = {
x: null,
y: null,
radius: 40
}
window.addEventListener('mousemove', e => {
mouse.x = event.x
mouse.y = event.y
})
function drawImage(width, height) {
let imageWidth = width
let imageHeight = height
const data = ctx.getImageData(0, 0, imageWidth, imageHeight)
class Particle {
constructor(x, y, color, size = 2) {
this.x = Math.round(x + canvas.width / 2 - imageWidth * 2)
this.y = Math.round(y + canvas.height / 2 - imageHeight * 2)
this.color = color
this.size = size
// Records base and previous positions to repaint the canvas to its original background color
this.baseX = Math.round(x + canvas.width / 2 - imageWidth * 2)
this.baseY = Math.round(y + canvas.height / 2 - imageHeight * 2)
this.previousX = null
this.previousY = null
this.density = (Math.random() * 100) + 2
}
stringifyColor() {
return `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${this.color.a}`
}
update() {
ctx.fillStyle = this.stringifyColor()
// collision detection
let dx = mouse.x - this.x
let dy = mouse.y - this.y
let distance = Math.sqrt(dx * dx + dy * dy)
let forceDirectionX = dx / distance
let forceDirectionY = dy / distance
// max distance, past that the force will be 0
const maxDistance = 100
let force = (maxDistance - distance) / maxDistance
if (force < 0) force = 0
let directionX = (forceDirectionX * force * this.density)
let directionY = (forceDirectionY * force * this.density)
this.previousX = this.x
this.previousY = this.y
if (distance < mouse.radius + this.size) {
this.x -= directionX
this.y -= directionY
} else {
// Rounded to one decimal number to as x and y cannot be the same (whole decimal-less integer)
// as baseX and baseY by decreasing using a random number / 20
if (Math.round(this.x) !== this.baseX) {
let dx = this.x - this.baseX
this.x -= dx / 20
}
if (Math.round(this.y) !== this.baseY) {
let dy = this.y - this.baseY
this.y -= dy / 20
}
}
}
}
function createParticle(x, y, size) {
if (data.data[(y * 4 * data.width) + (x * 4) + 3] > 128) {
let positionX = x
let positionY = y
let offset = (y * 4 * data.width) + (x * 4)
let color = {
r: data.data[offset],
g: data.data[offset + 1],
b: data.data[offset + 2],
a: data.data[offset + 3]
}
return new Particle(positionX * 4, positionY * 4, color, size)
}
}
// Instead of drawing each Particle one by one, construct an ImageData that can be
// painted into the canvas at once using putImageData()
function updateImageDataWith(particle) {
let x = particle.x
let y = particle.y
let prevX = particle.previousX
let prevY = particle.previousY
let size = particle.size
if (prevX || prevY) {
let prevMinY = Math.round(prevY - size)
let prevMaxY = Math.round(prevY + size)
let prevMinX = Math.round(prevX - size)
let prevMaxX = Math.round(prevX + size)
for (let y = prevMinY; y < prevMaxY; y++){
for (let x = prevMinX; x < prevMaxX; x++) {
if (y < 0 || y > canvasHeight) continue
else if (x < 0 || x > canvasWidth) continue
else {
let offset = y * 4 * canvasWidth + x * 4
imageData.data[offset] = 255
imageData.data[offset + 1] = 255
imageData.data[offset + 2] = 255
imageData.data[offset + 3] = 255
}
}
}
}
let minY = Math.round(y - size)
let maxY = Math.round(y + size)
let minX = Math.round(x - size)
let maxX = Math.round(x + size)
for (let y = minY; y < maxY; y++){
for (let x = minX; x < maxX; x++) {
if (y < 0 || y > canvasHeight) continue
else if (x < 0 || x > canvasWidth) continue
else {
let offset = y * 4 * canvasWidth + x * 4
imageData.data[offset] = particle.color.r
imageData.data[offset + 1] = particle.color.g
imageData.data[offset + 2] = particle.color.b
imageData.data[offset + 3] = particle.color.a
}
}
}
}
function init() {
particleArray = []
imageData = ctx.createImageData(canvasWidth, canvasHeight)
// Initializing imageData to a blank white "page"
for (let data = 1; data <= canvasWidth * canvasHeight * 4; data++) {
imageData.data[data - 1] = data % 4 === 0 ? 255 : 255
}
const size = 2 // Min size is 2
const step = Math.floor(size / 2)
for (let y = 0, y2 = data.height; y < y2; y += step) {
for (let x = 0, x2 = data.width; x < x2; x += step) {
// If particle's alpha value is too low, don't record it
if (data.data[(y * 4 * data.width) + (x * 4) + 3] > 128) {
let newParticle = createParticle(x, y, size)
particleArray.push(newParticle)
updateImageDataWith(newParticle)
}
}
}
}
function animate() {
requestAnimationFrame(animate)
for (let i = 0; i < particleArray.length; i++) {
let imageDataCanUpdateKey = `${Math.round(particleArray[i].x)}${Math.round(particleArray[i].y)}`
particleArray[i].update()
updateImageDataWith(particleArray[i])
}
ctx.putImageData(imageData, 0, 0)
}
init()
animate()
window.addEventListener('resize', e => {
canvas.width = innerWidth
canvas.height = innerHeight
canvasWidth = canvas.width
canvasHeight = canvas.height
init()
})
}
const png = new Image()
png.src = " data:image/png;base64,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"
window.addEventListener('load', e => {
// Ensuring height of image is always 100px
let pngWidth = png.width
let pngHeight = png.height
let divisor = pngHeight / 100
let finalWidth = pngWidth / divisor
let finalHeight = pngHeight / divisor
ctx.drawImage(png, 0, 0, finalWidth, finalHeight)
drawImage(finalWidth, finalHeight)
})
#canvas1 {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
<canvas id="canvas1"></canvas>
UPDATE 2: I have managed to optimize further. Now it can render FullHD image (1920x1080) without downgrading quality (on my PC it runs at about 20fps).
Take a look this code on JSFiddle (you can also tweak values).
Thanks also goes to #Richard (check out his answer) for idea to put all data in ImageData and make a single draw call. Code on JSFiddle is combination of his and mine optimizations (code below is my old code).
EDIT: Updated JSFiddle link, optimized more by spreading work of stationary particles across multiple frames (for given settings it improves performance for about 10%).
Regarding optimization, you won't achieve much by minifying code (in this case) because code that eats up CPU is runtime intensive (executes each frame). Minification is good for optimizing loading, not runtime execution.
Most of time is spent on drawing, and after some investigation I have found few performance optimizations but these are not enough to make big difference (eg. ctx.closePath() can be omitted and this saves some milliseconds).
What you can do is to either reduce resolution of image or skip some pixels in image in order to reduce work.
Additionally you could spread work across multiple frames to improve frame rate (but keep in mind if you spread it on more than few frames you might start seeing flickering).
Fullscreen issue can be solved by simply re-initializing everything on resize event.
Below is code with mentioned optimizations and fullscreen fix. Sample image is 375x375 pixels.
UPDATE: I played a little with code and I managed to improve further performance by optimizing calls (things I mentioned below code snippet). Code is updated with these changes.
var canv
var ctx
//performance critical parameters
const pixelStep = 2 //default 1; increase for images of higher resolution
const maxParticlesToProcessInOneFrame = 20000
//additional performance oriented paramteres
// Max distance, past that the force will be 0
const maxDistance = 144
const mouseRadius = 74
//customization parameters
const ctxFillStyle = 'rgba(0,0,0,.07)'
const speedOfActivatingParticle = 1
const speedOfRestoringParticle = 0.1
const png = new Image();
const mouse = {
x: null,
y: null
}
window.addEventListener('mousemove', (e) => {
mouse.x = event.x + canv.clientLeft;
mouse.y = event.y + canv.clientTop;
})
class Particle {
constructor(x, y, size) {
this.x = x
this.y = y
this.size = pixelStep
this.baseX = x
this.baseY = y
this.density = (Math.random() * 14) + 2
}
draw() {
//ctx.beginPath(); // this creates the sort of force field around the mouse pointer.
//ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.rect(this.x, this.y, this.size * 2, this.size * 2)
//ctx.closePath();
}
update() {
// Collision detection
let dx = mouse.x - this.x;
let dy = mouse.y - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
if (distance < mouseRadius + this.size) {
let forceDirectionX = dx / distance;
let forceDirectionY = dy / distance;
let force = (maxDistance - distance) / maxDistance;
if (force < 0)
force = 0;
const forceTimesDensity = force * this.density * speedOfActivatingParticle
let directionX = (forceDirectionX * forceTimesDensity);
let directionY = (forceDirectionY * forceTimesDensity); //Ch.this
this.x -= directionX;
this.y -= directionY;
} else {
if (this.x !== this.baseX) {
let dx = this.x - this.baseX;
this.x -= dx * speedOfRestoringParticle; // Speed Particles return to ori
}
if (this.y !== this.baseY) {
let dy = this.y - this.baseY;
this.y -= dy * speedOfRestoringParticle; // Speed Particles return to ori
}
}
this.draw();
}
}
window.addEventListener('resize', initializeCanvas)
window.addEventListener("load", initializeCanvas, {
once: true
})
let animationFrame = null
function initializeCanvas(e) {
cancelAnimationFrame(animationFrame)
canv = document.getElementById('Canv');
ctx = canv.getContext('2d');
canv.width = window.innerWidth;
canv.height = window.innerHeight;
let particles = {}
function drawImage() {
let imageWidth = png.width; //These to values crop if / sum no.
let imageHeight = png.height;
const data = ctx.getImageData(0, 0, imageWidth, imageHeight); //Gets img data for particles
ctx.clearRect(0, 0, canv.width, canv.height); // Clears the original img as its now being stored in the variable data.
function init() {
particles = {}
for (let y = 0, y2 = data.height; y < y2; y += pixelStep) {
for (let x = 0, x2 = data.width; x < x2; x += pixelStep) {
if (data.data[(y * 4 * data.width) + (x * 4) + 3] > 128) {
let positionX = x
let positionY = y
let color = "rgb(" + data.data[(y * 4 * data.width) + (x * 4)] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 1] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 2] + ")";
let particlesArray = particles[color]
if (!particlesArray)
particlesArray = particles[color] = []
particlesArray.push(new Particle(positionX * 2, positionY * 2))
} /* These number effect png size but its to high */
}
}
}
let particlesProcessed = 0
let animateGenerator = animate()
function* animate() {
particlesProcessed = 0
ctx.fillStyle = ctxFillStyle;
ctx.fillRect(0, 0, innerWidth, innerHeight);
let colors = Object.keys(particles)
for (let j = 0; j < colors.length; j++) {
let color = colors[j]
ctx.fillStyle = color
let particlesArray = particles[color]
ctx.beginPath()
for (let i = 0; i < particlesArray.length; i++) {
particlesArray[i].update()
if (++particlesProcessed > maxParticlesToProcessInOneFrame) {
particlesProcessed = 0
ctx.fill()
yield
ctx.beginPath()
}
}
ctx.fill()
}
}
init();
function animateFrame() {
animationFrame = requestAnimationFrame(() => {
if (animateGenerator.next().done) {
animateGenerator = animate()
}
animateFrame()
})
}
animateFrame()
}
console.log('page has loaded');
ctx.drawImage(png, 0, 0, png.width, png.height);
drawImage();
}
png.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXcAAAF3CAMAAABpHvvMAAAC1lBMVEUAAAAAAAAAAADp6ekAAAAAAACcnJwAAAAAAACgoKAAAAAAAAAAAAAAAADq6uoAAAAAAADu7u6Ojo7d3d0AAABISEj5+fmYmJi2traampr6+vqXl5ebm5u3t7dNTU1HR0eRkZG8vLzd3d3f39+zs7NTU1Pw8PDk5OTu7u7////VuTP54C3r6uoAAADVuDDr6+vYvkL38db54Cv12y7r6+3avjL8/PzXuzPw1i/54Cr///3u7e3Wuzn99sP54TTWujcyMjLl3bf43y354TLk27H//vnZwEnWujXh4eH99b3VuTT875YlJSX//vv19fXXvD0GBgZHR0fawU39/Pfr6ufZv0X+/PT7+OnYvDL69uTdx1v8+e7cxVXYvUDy2S78+vH489tbW1vq6OPhzWz64zzu1C9DQ0PXvDvq59yxsbGFhYXfyWHm377cyW7gy2fbw1D65EP//fD+/Ory6b7x57jg0Ij54jcKCgr09PT59N/v5K/t4abezHfZwlTpzzD39/f+++T07Mfp2ZDbxmDr0S/65UkfHx/q6ODfzn/j0HXaxFr65VDgxTHfwzHn4snk2avq3Jnh1Jbiz3HixzH54S/o5NHz68Lh0pD874387oXk0Xn763VsbGzbx2dPT0/cwDL//vbp5dX17864uLjr3p3l1IL87H376m376GD651vnzDD5+fn++t7b29v++dT28dT28NHn4MTs36Hp25Tl033o48717cv+98f99bfw5bP98qjn2Iv76Wf65lQ8PDzdwjL++tju4qr98qHXuzoVFRXn5+fp5tn9867j2Kb88Zzn1ojm1YV9fX12dnbz8/P99LPi1qDlyzAQEBDX19f++M/Nzc3++MvExMTi1ZwrKysaGhrw8PDT09OsrKxWVlbkyTGXl5eQkJD54TO1tbXi16NkZGSnp6eKioqCgoLlyjGdnZ3j4+OkpKRKSkrkyjCpqank1pD353rMpemXAAAAKXRSTlMABAvSFCRoCA5mIh8RFtEnG95Wtxg39GuHavRsaYU1OVSCuLZyMd3D3S9YQ4oAABlBSURBVHja7NQBzoMgDIZh8UcpKzoEtyUmIxCnife/4F+9BAb63KBvvrRhjDHGGGOM1UoUq7knUYnmRsRFKdUWjQ68U3tBqDiA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";
body {
margin: 0;
padding: 0;
}
#Canv {
width: 100vw;
height: 100vh;
filter: blur(1.5px);
}
<canvas id="Canv"></canvas>
If you still need to optimize, you could do some optimization regarding ctx.beginPath(), ctx.fill() and ctx.rect() calls. For example, try to combine sibling pixels (pixels that are next to each other) and render them all in one call. Furthermore, you could merge similar colors in single color, but downside is that image will loose quality (depending on how much colors are merged).
Also (if this is option) you might want to set fixed canvas size rather than dynamically sized.
Disclosure: On my PC given code works nicely, but on others it might still have performance issues. For that reason try to play with pixelStep and maxParticlesToProcessInOneFrame variable values.
I need to generate random non-overlapping polygons without free spaces (. They can have any shape.
The only thing I found is an example that uses p5.js to create only non-overlapping circles, but free spaces are present.
Does anyone know how to do it?
// Uses P5.js for canvas creation and drawing
function setup() {
var circles = [],
circle = {},
overlapping = false,
NumCircles = 4000,
protection = 1000,
counter = 0,
canvasWidth = window.innerWidth,
canvasHeight = window.innerHeight;
createCanvas(canvasWidth, canvasHeight);
// populate circles array
// brute force method continues until # of circles target is reached
// or until the protection value is reached
while (circles.length < NumCircles &&
counter < protection) {
circle = {
x: random(width),
y: random(height),
r: random(3, 36)
};
overlapping = false;
// check that it is not overlapping with any existing circle
// another brute force approach
for (var i = 0; i < circles.length; i++) {
var existing = circles[i];
var d = dist(circle.x, circle.y, existing.x, existing.y)
if (d < circle.r + existing.r) {
// They are overlapping
overlapping = true;
// do not add to array
break;
}
}
// add valid circles to array
if (!overlapping) {
circles.push(circle);
}
counter++;
}
// circles array is complete
// draw canvas once
background("#233")
fill("#2AC1A6");
noStroke();
for (var i = 0; i < circles.length; i++) {
ellipse(circles[i].x, circles[i].y,
circles[i].r*2, circles[i].r*2);
}
}
* {
padding: 0;
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js"></script>
Your question is not very specific...
random non-overlapping polygons without free spaces
Rectangles are polygons, let's make a bunch of those with random sizes and colors
Here is a starting point:
canvas = document.getElementById('c');
ctx = canvas.getContext('2d');
var colors = ["red", "blue", "orange", "green", "yellow"]
h = Math.floor(canvas.height / Math.floor(Math.random() * 15 + 4))
for (y = 0; y < canvas.height; y += h) {
w = Math.floor(canvas.width / Math.floor(Math.random() * 15 + 4))
for (x = 0; x < canvas.width; x += w) {
c = colors[Math.floor(Math.random() * 10) % 5]
draw(x, y, w, h, c)
}
}
function draw(x, y, w, h, color) {
ctx.beginPath();
ctx.rect(x, y, w, h)
ctx.stroke();
ctx.fillStyle = color
ctx.fill();
}
<canvas id="c"></canvas>
Here is a function to help you draw "random" polygons:
var ctx = document.getElementById('c').getContext('2d');
function polygon(sides, size, Xcenter, Ycenter, offset) {
ctx.beginPath();
ctx.moveTo(Xcenter + size * Math.cos(offset), Ycenter + size * Math.sin(offset));
for (var i = 1; i <= sides; i += 1) {
x = Xcenter + size * Math.cos(i * 2 * Math.PI / sides +offset)
y = Ycenter + size * Math.sin(i * 2 * Math.PI / sides +offset)
ctx.lineTo(x, y);
}
ctx.stroke();
}
arr = [3, 4, 5, 6, 7];
arr.sort(() => Math.random() - 0.5);
for (var i = 1; i <= 5; i += 1) {
offset = Math.random() * 5
polygon(arr[i-1], 40, 85*i, 60, offset)
}
<canvas id="c" width=600></canvas>
There is something I need to build, but my math ability is not up to par. What I am looking to build is something like this demo, but I need it to be a hybrid of a circle and polygon instead of a line, so to speak. The black line should be dynamic and randomly generated that basically acts as a border on the page.
Currently, I am dissecting this answer with the aim of hopefully being able to transpose it into this, but I am having massive doubts that I will be able to figure this out.
Any idea how to do this or can anybody explain the mathematics?
Below are my notes about the code from the answer I linked above.
var
cw = cvs.width = window.innerWidth,
ch = cvs.height = window.innerHeight,
cx = cw / 2,
cy = ch / 2,
xs = Array(),
ys = Array(),
npts = 20,
amplitude = 87, // can be val from 1 to 100
frequency = -2, // can be val from -10 to 1 in steps of 0.1
ctx.lineWidth = 4
// creates array of coordinates that
// divides page into regular portions
// creates array of weights
for (var i = 0; i < npts; i++) {
xs[i] = (cw/npts)*i
ys[i] = 2.0*(Math.random()-0.5)*amplitude
}
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
for (let x = 0; x < cw; x++) {
y = 0.0
wsum = 0.0
for (let i = -5; i <= 5; i++) {
xx = x; // 0 / 1 / 2 / to value of screen width
// creates sequential sets from [-5 to 5] to [15 to 25]
ii = Math.round(x/xs[1]) + i
// `xx` is a sliding range with the total value equal to client width
// keeps `ii` within range of 0 to 20
if (ii < 0) {
xx += cw
ii += npts
}
if (ii >= npts){
xx -= cw
ii -= npts
}
// selects eleven sequential array items
// which are portions of the screen width and height
// to create staggered inclines in increments of those portions
w = Math.abs(xs[ii] - xx)
// creates irregular arcs
// based on the inclining values
w = Math.pow(w, frequency)
// also creates irregular arcs therefrom
y += w*ys[ii];
// creates sets of inclining values
wsum += w;
}
// provides a relative position or weight
// for each y-coordinate in the total path
y /= wsum;
//y = Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y+cy);
}
ctx.stroke();
}
Draw();
This is my answer. Please read the comments in the code. I hope this is what you need.
// initiate the canvas
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let cw = (canvas.width = 600),
cx = cw / 2;
let ch = (canvas.height = 400),
cy = ch / 2;
ctx.fillStyle = "white"
// define the corners of an rectangle
let corners = [[100, 100], [500, 100], [500, 300], [100, 300]];
let amplitud = 20;// oscilation amplitude
let speed = 0.01;// the speed of the oscilation
let points = []; // an array of points to draw the curve
class Point {
constructor(x, y, hv) {
// the point is oscilating around this point (cx,cy)
this.cx = x;
this.cy = y;
// the current angle of oscilation
this.a = Math.random() * 2 * Math.PI;
this.hv = hv;// a variable to know if the oscilation is horizontal or vertical
this.update();
}
// a function to update the value of the angle
update() {
this.a += speed;
if (this.hv == 0) {
this.x = this.cx;
this.y = this.cy + amplitud * Math.cos(this.a);
} else {
this.x = this.cx + amplitud * Math.cos(this.a);
this.y = this.cy;
}
}
}
// a function to divide a line that goes from a to b in n segments
// I'm using the resulting points to create a new point object and push this new point into the points array
function divide(n, a, b) {
for (var i = 0; i <= n; i++) {
let p = {
x: (b[0] - a[0]) * i / n + a[0],
y: (b[1] - a[1]) * i / n + a[1],
hv: b[1] - a[1]
};
points.push(new Point(p.x, p.y, p.hv));
}
}
divide(10, corners[0], corners[1]);points.pop();
divide(5, corners[1], corners[2]);points.pop();
divide(10, corners[2], corners[3]);points.pop();
divide(5, corners[3], corners[0]);points.pop();
// this is a function that takes an array of points and draw a curved line through those points
function drawCurves() {
//find the first midpoint and move to it
let p = {};
p.x = (points[points.length - 1].x + points[0].x) / 2;
p.y = (points[points.length - 1].y + points[0].y) / 2;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
//curve through the rest, stopping at each midpoint
for (var i = 0; i < points.length - 1; i++) {
let mp = {};
mp.x = (points[i].x + points[i + 1].x) / 2;
mp.y = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, mp.x, mp.y);
}
//curve through the last point, back to the first midpoint
ctx.quadraticCurveTo(
points[points.length - 1].x,
points[points.length - 1].y,
p.x,
p.y
);
ctx.stroke();
ctx.fill();
}
function Draw() {
window.requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
points.map(p => {
p.update();
});
drawCurves();
}
Draw();
canvas{border:1px solid; background:#6ab150}
<canvas></canvas>
I'm new to HTML5 Canvas and I'm trying to draw a triangle with rounded corners.
I have tried
ctx.lineJoin = "round";
ctx.lineWidth = 20;
but none of them are working.
Here's my code:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
var x = 18 / 2;
var y = 0;
var triangleWidth = 18;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
Could you help me?
Rounding corners
An invaluable function I use a lot is rounded polygon. It takes a set of 2D points that describe a polygon's vertices and adds arcs to round the corners.
The problem with rounding corners and keeping within the constraint of the polygons area is that you can not always fit a round corner that has a particular radius.
In these cases you can either ignore the corner and leave it as pointy or, you can reduce the rounding radius to fit the corner as best possible.
The following function will resize the corner rounding radius to fit the corner if the corner is too sharp and the lines from the corner not long enough to get the desired radius in.
Note the code has comments that refer to the Maths section below if you want to know what is going on.
roundedPoly(ctx, points, radius)
// ctx is the context to add the path to
// points is a array of points [{x :?, y: ?},...
// radius is the max rounding radius
// this creates a closed polygon.
// To draw you must call between
// ctx.beginPath();
// roundedPoly(ctx, points, radius);
// ctx.stroke();
// ctx.fill();
// as it only adds a path and does not render.
function roundedPoly(ctx, points, radiusAll) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius;
// convert 2 points into vector form, polar form, and normalised
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
radius = radiusAll;
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
// for each point
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
//-----------------------------------------
// Part 1
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA < -1 ? -1 : sinA > 1 ? 1 : sinA);
//-----------------------------------------
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
if(p2.radius !== undefined){
radius = p2.radius;
}else{
radius = radiusAll;
}
//-----------------------------------------
// Part 2
halfAngle = angle / 2;
//-----------------------------------------
//-----------------------------------------
// Part 3
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
//-----------------------------------------
//-----------------------------------------
// Special part A
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
//-----------------------------------------
// Part 4
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
//-----------------------------------------
// Part 5
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
//-----------------------------------------
// Part 6
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
//-----------------------------------------
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
You may wish to add to each point a radius eg {x :10,y:10,radius:20} this will set the max radius for that point. A radius of zero will be no rounding.
The maths
The following illistration shows one of two possibilities, the angle to fit is less than 90deg, the other case (greater than 90) just has a few minor calculation differences (see code).
The corner is defined by the three points in red A, B, and C. The circle radius is r and we need to find the green points F the circle center and D and E which will define the start and end angles of the arc.
First we find the angle between the lines from B,A and B,C this is done by normalising the vectors for both lines and getting the cross product. (Commented as Part 1) We also find the angle of line BC to the line at 90deg to BA as this will help determine which side of the line to put the circle.
Now we have the angle between the lines, we know that half that angle defines the line that the center of the circle will sit F but we do not know how far that point is from B (Commented as Part 2)
There are two right triangles BDF and BEF which are identical. We have the angle at B and we know that the side DF and EF are equal to the radius of the circle r thus we can solve the triangle to get the distance to F from B
For convenience rather than calculate to F is solve for BD (Commented as Part 3) as I will move along the line BC by that distance (Commented as Part 4) then turn 90deg and move up to F (Commented as Part 5) This in the process gives the point D and moving along the line BA to E
We use points D and E and the circle center F (in their abstract form) to calculate the start and end angles of the arc. (done in the arc function part 6)
The rest of the code is concerned with the directions to move along and away from lines and which direction to sweep the arc.
The code section (special part A) uses the lengths of both lines BA and BC and compares them to the distance from BD if that distance is greater than half the line length we know the arc can not fit. I then solve the triangles to find the radius DF if the line BD is half the length of shortest line of BA and BC
Example use.
The snippet is a simple example of the above function in use. Click to add points to the canvas (needs a min of 3 points to create a polygon). You can drag points and see how the corner radius adapts to sharp corners or short lines. More info when snippet is running. To restart rerun the snippet. (there is a lot of extra code that can be ignored)
The corner radius is set to 30.
const ctx = canvas.getContext("2d");
const mouse = {
x: 0,
y: 0,
button: false,
drag: false,
dragStart: false,
dragEnd: false,
dragStartX: 0,
dragStartY: 0
}
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
const lb = mouse.button;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
if (lb !== mouse.button) {
if (mouse.button) {
mouse.drag = true;
mouse.dragStart = true;
mouse.dragStartX = mouse.x;
mouse.dragStartY = mouse.y;
} else {
mouse.drag = false;
mouse.dragEnd = true;
}
}
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const pointOnLine = {x:0,y:0};
function distFromLines(x,y,minDist){
var index = -1;
const v1 = {};
const v2 = {};
const v3 = {};
const point = P2(x,y);
eachOf(polygon,(p,i)=>{
const p1 = polygon[(i + 1) % polygon.length];
v1.x = p1.x - p.x;
v1.y = p1.y - p.y;
v2.x = point.x - p.x;
v2.y = point.y - p.y;
const u = (v2.x * v1.x + v2.y * v1.y)/(v1.y * v1.y + v1.x * v1.x);
if(u >= 0 && u <= 1){
v3.x = p.x + v1.x * u;
v3.y = p.y + v1.y * u;
dist = Math.hypot(v3.y - point.y, v3.x - point.x);
if(dist < minDist){
minDist = dist;
index = i;
pointOnLine.x = v3.x;
pointOnLine.y = v3.y;
}
}
})
return index;
}
function roundedPoly(ctx, points, radius) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut;
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA); // warning you should guard by clampling
// to -1 to 1. See function roundedPoly in answer or
// Math.asin(Math.max(-1, Math.min(1, sinA)))
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
halfAngle = angle / 2;
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
const eachOf = (array, callback) => { var i = 0; while (i < array.length && callback(array[i], i++) !== true); };
const P2 = (x = 0, y = 0) => ({x, y});
const polygon = [];
function findClosestPointIndex(x, y, minDist) {
var index = -1;
eachOf(polygon, (p, i) => {
const dist = Math.hypot(x - p.x, y - p.y);
if (dist < minDist) {
minDist = dist;
index = i;
}
});
return index;
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var dragPoint;
var globalTime;
var closestIndex = -1;
var closestLineIndex = -1;
var cursor = "default";
const lineDist = 10;
const pointDist = 20;
var toolTip = "";
// main update function
function update(timer) {
globalTime = timer;
cursor = "crosshair";
toolTip = "";
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth - 4 || h !== innerHeight - 4) {
cw = (w = canvas.width = innerWidth - 4) / 2;
ch = (h = canvas.height = innerHeight - 4) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
if (mouse.drag) {
if (mouse.dragStart) {
mouse.dragStart = false;
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex === -1){
polygon.push(dragPoint = P2(mouse.x, mouse.y));
}else{
polygon.splice(closestLineIndex+1,0,dragPoint = P2(mouse.x, mouse.y));
}
}else{
dragPoint = polygon[closestIndex];
}
}
dragPoint.x = mouse.x;
dragPoint.y = mouse.y
cursor = "none";
}else{
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex > -1){
toolTip = "Click to cut line and/or drag to move.";
}
}else{
toolTip = "Click drag to move point.";
closestLineIndex = -1;
}
}
ctx.lineWidth = 4;
ctx.fillStyle = "#09F";
ctx.strokeStyle = "#000";
ctx.beginPath();
roundedPoly(ctx, polygon, 30);
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.lineWidth = 0.5;
eachOf(polygon, p => ctx.lineTo(p.x,p.y) );
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = "orange";
ctx.lineWidth = 1;
eachOf(polygon, p => ctx.strokeRect(p.x-2,p.y-2,4,4) );
if(closestIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
dragPoint = polygon[closestIndex];
ctx.strokeRect(dragPoint.x-4,dragPoint.y-4,8,8);
cursor = "move";
}else if(closestLineIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
var p = polygon[closestLineIndex];
var p1 = polygon[(closestLineIndex + 1) % polygon.length];
ctx.beginPath();
ctx.lineTo(p.x,p.y);
ctx.lineTo(p1.x,p1.y);
ctx.stroke();
ctx.strokeRect(pointOnLine.x-4,pointOnLine.y-4,8,8);
cursor = "pointer";
}
if(toolTip === "" && polygon.length < 3){
toolTip = "Click to add a corners of a polygon.";
}
canvas.title = toolTip;
canvas.style.cursor = cursor;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border: 2px solid black;
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
I started by using #Blindman67 's answer, which works pretty well for basic static shapes.
I ran into the problem that when using the arc approach, having two points right next to each other is very different than having just one point. With two points next to each other, it won't be rounded, even if that is what your eye would expect. This is extra jarring if you are animating the polygon points.
I fixed this by using Bezier curves instead. IMO this is conceptually a little cleaner as well. I just make each corner with a quadratic curve where the control point is where the original corner was. This way, having two points in the same spot is virtually the same as only having one point.
I haven't compared performance but seems like canvas is pretty good at drawing Beziers.
As with #Blindman67 's answer, this doesn't actually draw anything so you will need to call ctx.beginPath() before and ctx.stroke() after.
/**
* Draws a polygon with rounded corners
* #param {CanvasRenderingContext2D} ctx The canvas context
* #param {Array} points A list of `{x, y}` points
* #radius {number} how much to round the corners
*/
function myRoundPolly(ctx, points, radius) {
const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
const lerp = (a, b, x) => a + (b - a) * x
const lerp2D = (p1, p2, t) => ({
x: lerp(p1.x, p2.x, t),
y: lerp(p1.y, p2.y, t)
})
const numPoints = points.length
let corners = []
for (let i = 0; i < numPoints; i++) {
let lastPoint = points[i]
let thisPoint = points[(i + 1) % numPoints]
let nextPoint = points[(i + 2) % numPoints]
let lastEdgeLength = distance(lastPoint, thisPoint)
let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius)
let start = lerp2D(
thisPoint,
lastPoint,
lastOffsetDistance / lastEdgeLength
)
let nextEdgeLength = distance(nextPoint, thisPoint)
let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius)
let end = lerp2D(
thisPoint,
nextPoint,
nextOffsetDistance / nextEdgeLength
)
corners.push([start, thisPoint, end])
}
ctx.moveTo(corners[0][0].x, corners[0][0].y)
for (let [start, ctrl, end] of corners) {
ctx.lineTo(start.x, start.y)
ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y)
}
ctx.closePath()
}
Styles for joining of lines such as ctx.lineJoin="round" apply to the stroke operation on paths - which is when their width, color, pattern, dash/dotted and similar line style attributes are taken into account.
Line styles do not apply to filling the interior of a path.
So to affect line styles a stroke operation is needed. In the following adaptation of posted code, I've translated canvas output to see the result without cropping, and stroked the triangle's path but not the rectangles below it:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
ctx.translate( 18, 12);
var x = 18 / 2;
var y = 0;
var triangleWidth = 48;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
// stroke the triangle path.
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.strokeStyle = "orange";
ctx.stroke();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>