Freehand creation of HTML objects Canva
I am creating a template in HTML canvas to call.
How would I randomly generate shapes like the ones coded below to form a pattern like the image. I created one version by creating code with drawscript in Illustrator, but it is far from perfect, how could I do the same with a loop?
thank
//triangles
ctx.fillStyle="rgb(75,128,166)";
ctx.beginPath();
ctx.moveTo(824,92);
ctx.lineTo(796,140);
ctx.lineTo(767,92);
ctx.lineTo(824,92);
ctx.fill();
//circles
ctx.fillStyle="rgba(35,121,67,0.8)";
ctx.beginPath();
ctx.moveTo(869,263);
ctx.bezierCurveTo(869,253,861,244,850,244);
ctx.bezierCurveTo(839,244,831,253,831,263);
ctx.bezierCurveTo(831,274,839,283,850,283);
ctx.bezierCurveTo(861,283,869,274,869,263);
ctx.fill();
source to share
You can make a square grid that is skewed for triangles, then have a random padding as well as a random diagonal subdivision of that rectangle to make it look like a triangle.
For circles, we can use a pseudo-hexagonal system, which means that the circles will be placed in hexagonal positions, just offsetting the circular encirclement and not the actual hexagon.
Triangles
- First define a simple mesh other than one that overlaps the drawing area (this is one approach, an alternative approach is to wrap the coordinates, but this requires transformation tracking, so brute force in this example)
- Skew transform to rotate vertical alignment diagonally.
- Determine coverage, fill cells, which in turn determine if they are separated, if the top, etc.
How to cover with random cells can be done in several ways, below is just one approach. Others can use a fixed grid system and iterate over it using a sweep based step (tracking is required to ensure accuracy). Third to fill the coverage of the cells, then an arbitrary sort of the array to shuffle the cells around.
Circles
A grid will also be used here, but since we're going to pack the vertical space to get closer to the hex grid, we need to compensate for that. The layout will be made taking into account the following factors:
- vertical distance = diameter x sqrt (3) x 0.5 1)
- horizontal distance = radius (offset toggles every 2nd row)
( 1) thanks to @Jason and his answer for reminding me of this!)
To compensate for the vertical "packed" circles, since they won't fill the bottom, we use the inverse sqrt (3) * 0.5 ( 1 / (sqrt(3) * 0.5)
).
Final result
Combining both of them into one canvas will result in this:
var canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d"),
w = canvas.width,
h = canvas.height,
cellsY = 14, // cells Y for triangles
cellsX = cellsY * 2, // cells X times two to overlap skew
cw = w / cellsX * 2, // cell width and height
ch = h / cellsY,
toggle, cx = 0, cy, // for circles
cells = 25, // cells for cirles + comp. (see below)
deltaY = 0.8660254037844386, // = sqrt(3) * 0.5
deltaYI = 1 / deltaY, // inverse deltaY
grid = new Uint8Array((cells * cells * deltaYI)|0), // circles "booleans"
i;
// Calc and Render Triangles ---
// main transform: skew
ctx.setTransform(1, 0, 0.51, 1, -cellsX * cw * 0.5, 0);
ctx.fillStyle = "rgb(90, 146, 176)";
// fill random cells based on likely cover:
var cover = 0.67, // how much of total area to cover
biasDiv = 0.6, // bias for splitting cell
biasUpper = 0.5, // bias for which part to draw
count = cellsX * cellsY * cover, // coverage
tris = [],
x, y, d, u, overlap; // generate cells
for (i = 0; i < count; i++) {
overlap = true;
while (overlap) { // if we have overlapping cells
x = (Math.random() * cellsX) | 0;
y = (Math.random() * cellsY) | 0;
overlap = hasCell(x, y);
if (!overlap) {
d = Math.random() < biasDiv; // divide cell?
u = Math.random() < biasUpper; // if divided, use upper part?
tris.push({
x: x,
y: y,
divide: d,
upper: u
})
}
}
}
function hasCell(x, y) {
for (var i = 0, c; c = tris[i++];) {
if (c.x === x && c.y === y) return true;
}
return false;
}
// render
for (i = 0; i < tris.length; i++) renderTri(tris[i]);
ctx.fill(); // fill all sub-paths
function renderTri(t) {
var x = t.x * cw, // convert to abs. position
y = t.y * ch;
if (t.divide) { // create triangle
ctx.moveTo(x + cw, y); // define common diagonal
ctx.lineTo(x, y + ch);
t.upper ? ctx.lineTo(x, y) : ctx.lineTo(x + cw, y + ch);
}
else {
ctx.rect(x, y, cw, ch); // fill complete cell
}
}
// Calc and Render Circles ---
cover = 0.5, // how much of total area to cover
count = Math.ceil(grid.length * cover); // coverage
cw = ch = w / cells;
ctx.setTransform(1,0,0,1,0,0); // reset transforms
ctx.fillStyle = "rgb(32, 141, 83)";
ctx.globalCompositeOperation = "multiply"; // blend mode instead of alpha
if (ctx.globalCompositeOperation !== "multiply") ctx.globalAlpha = 0.5; // for IE
for (i = 0; i < count; i++) {
overlap = true;
while (overlap) { // if we have overlapping cells
x = (Math.random() * cells) | 0; // x index
y = (Math.random() * cells * deltaYI) | 0; // calc y index + comp
overlap = hasCircle(x, y); // already has circle?
if (!overlap) {
grid[y * cells + x] = 1; // set "true"
}
}
}
function hasCircle(x, y) {
return grid[y * cells + x] === 1;
}
// render
ctx.beginPath();
cy = ch * 0.5; // start on Y axis
for (y = 0; y < (cells * deltaYI)|0; y++) { // iterate rows + comp.
toggle = !(y % 2); // toggle x offset
for (x = 0; x < cells; x++) { // columns
if (grid[y * cells + x]) { // has circle?
cx = x * cw + (toggle ? cw * 0.5 : 0); // calc x
ctx.moveTo(cx + cw * 0.5, cy); // creat sub-path
ctx.arc(cx, cy, cw * 0.5, 0, 2 * Math.PI); // add arc
ctx.closePath(); // close sub-path
}
}
cy += ch * deltaY; // add deltaY
}
ctx.fill(); // fill all at once
body {background:#777}
canvas {padding:50px;background: rgb(226, 226, 226)}
<canvas width=600 height=600></canvas>
There is room for refactoring here and the randomization functions are not the best in terms of performance, but it should be enough to get you going. Hope this helps!
source to share
Tip: Creating circles is easier with context.arc
rather than adding 4 Bezier curves (and 4 Bezier curves do not form a perfect circle, for example arc
).
Adding random circles
If you want more randomized coverage of the circles, you should try adding one circle at a time and make sure that each new attempt does not overlap existing circles.
Here's some sample code and demo that adds as many random circles as needed to cover 40% of the canvas area:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var PI2=Math.PI*2;
var radius=10;
var radiusTest=(2*radius)*(2*radius);
var circleCoverageDesired=.40;
var circleCount=parseInt((cw*ch*circleCoverageDesired)/(Math.PI*radius*radius))+1;
var circles=[];
ctx.fillStyle='green';
ctx.globalAlpha=0.25;
addRandomCircles();
function addRandomCircles(){
// give up after "tries" to avoid unsolvable patterns
var tries=circleCount*200;
while(tries>0 && circles.length<circleCount){
var x=Math.random()*(cw-radius*2)+radius/2;
var y=Math.random()*(ch-radius*2)+radius/2;
testRandomCircle(x,y);
tries--;
}
}
function testRandomCircle(x,y){
for(var i=0;i<circles.length;i++){
var c=circles[i];
var dx=x-c.x;
var dy=y-c.y;
if(dx*dx+dy*dy<=radiusTest){
return(false);
}
}
var circle={x:x,y:y};
circles.push(circle);
ctx.beginPath();
ctx.arc(x,y,radius,0,PI2);
ctx.closePath();
ctx.fill();
var pct=parseInt((Math.PI*radius*radius*circles.length)/(cw*ch)*100);
$('#count').text('Added: '+circles.length+' of '+circleCount+' needed circles for '+pct+'% coverage.');
return(true);
}
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4 id=count>Count</h4>
<canvas id="canvas" width=300 height=300></canvas>
Adding random triangles
Adding random triangles requires the same constraint as adding random circles. You have to add one new triangle at a time and make sure that each new triangle does not overlap existing triangles.
Testing if the overlap of two polygons (like triangles) can be done using the dividing axis theorem,
A previous Stackoverflow answer by Matthias Buelens showed how the Separating Axis Theorem: javascript polygon intersection could be implemented
source to share
OK Here's a function that will render this art style on any canvas on the page (with shape size and frequency, and canvas size):
function art(options, canvas) {
var surface = document.getElementById(canvas),
context = surface.getContext("2d"),
row,
col,
triangleDirection = 1,
triangleSize = options.triangle.size,
circleSize = options.circle.size,
circleStep = Math.sqrt(3) * circleSize * 2,
circleOffset = 0;
function shouldDraw(chances) {
return Math.random() < chances;
}
function drawTriangle(x, y, direction, size, ctx) {
ctx.fillStyle = options.triangle.color;
ctx.beginPath();
ctx.moveTo(x, y - (direction * size));
ctx.lineTo(x - (direction * size), y + (direction * size));
ctx.lineTo(x + (direction * size), y + (direction * size));
ctx.lineTo(x, y - (direction * size));
ctx.fill();
ctx.strokeStyle = options.triangle.color;
ctx.stroke();
}
function drawCircle(x, y, size, ctx) {
//circles
ctx.fillStyle = options.circle.color;
ctx.beginPath();
ctx.arc(x, y, size, 0, 2 * Math.PI, false);
ctx.fill();
}
//Draw Tiangles
for (col = 1; col < (surface.width / triangleSize); col++) {
for (row = 1; row < (surface.height / triangleSize); row++) {
if (shouldDraw(options.triangle.density)) {
drawTriangle(row * triangleSize, col * triangleSize * 2, triangleDirection, triangleSize, context);
}
//Swap direction
triangleDirection = -1 * triangleDirection;
}
}
//Draw Circles
for (row = 1; row < (surface.height / circleSize) - 1; row++) {
for (col = 1; col < (surface.width / circleStep) - 1; col++) {
if (shouldDraw(options.circle.density)) {
drawCircle((row * circleSize), (col * circleStep) + circleOffset, circleSize, context);
}
}
//swap offset by row
if (row % 2 === 0) {
circleOffset = circleStep / 2;
} else {
circleOffset = 0;
}
}
}
art({triangle: {size:24, density: 0.7, color: 'rgb(75,128,166)'}, circle: {size: 14, density: 0.2, color: 'rgba(35,121,67,0.8)'}}, 'surface')
#surface {
width: 600px;
height: 600px;
}
<canvas id='surface' height='600' width='600' />
Here are some highlights:
Drawing a triangle is an oscillating drawing of triangles up and down, so generalizing your triangular code to a function that can draw this based on arguments will make your code more convenient.
The circle pattern wobbles, but this time its line by line and shifts left and right. To figure this out, I needed to clear some underlying geometry. Looking at three circles above each other:
you can see their height moving in steps equal to the radius ( circleSize
in code). However, the distance they are from each other is more complicated, however, when you see it as an equilateral triangle, you can calculate it as the height of that triangle, or:
then you can see that the variables should be located at a distance twice this distance, which, after decreasing the fractions, turns into: Math.sqrt(3) * circleSize * 2
Hope it helps :)
source to share