<p>Click on the grid to draw a line</p>
<button onClick="clearLines()">Clear lines</button>
<button onClick="init()">Restart</button>
<label>Draw dots: <input class="draw-dots" type="checkbox" /></label>
<label>Draw lines: <input class="draw-lines" type="checkbox" checked /></label>
body {
padding: 20px;
font-family: Helvetica, Arial, sans-serif;
color: #333;
}
p {
margin-bottom: 10px;
}
svg {
overflow: visible;
margin: 50px auto 20px;
max-width: 800px;
display: block;
position: relative;
}
marker {
overflow: visible;
}
.coordinate-line {
stroke: #ddd;
}
marker,
.vector {
stroke: #abc;
fill: #abc;
}
View Compiled
const gridStep = 20;
const gridSize = 15;
const maxVelocity = gridStep;
const size = gridSize * gridStep;
const drawDotsInput = document.querySelector('.draw-dots');
const drawDotsLines = document.querySelector('.draw-lines');
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", `0 0 ${size} ${size}`);
document.querySelector("body").appendChild(svg);
const vectors = [];
const directionVector = {
x: maxVelocity * 0.5,
y: 0
};
function getRandomVector() {
const xSign = Math.random() > 0.5 ? 1 : -1;
const ySign = Math.random() > 0.5 ? 1 : -1;
return {
x: Math.random() * xSign * maxVelocity + directionVector.x,
y: Math.random() * ySign * maxVelocity + directionVector.y
};
}
function roundToDecimal(number, decimalPlaces = 2) {
return parseFloat(number.toFixed(decimalPlaces));
}
function drawDot(dot, color = "#333") {
if (drawDotsInput.checked) {
svg.innerHTML += `
<circle
class="generated-dot"
fill="${color}"
cx="${dot.x}"
cy="${dot.y}"
r="${size / 200}"
/>
`;
}
}
function addVectors(v1, v2) {
return {
x: v1.x + v2.x,
y: v1.y + v2.y
};
}
function multiplyVector(v, factor) {
return {
x: v.x * factor,
y: v.y * factor
};
}
function getDistance(v1, v2) {
const x = v1.x - v2.x;
const y = v1.y - v2.y;
return Math.sqrt(x * x + y * y) / gridStep;
}
function drawNextDot(dot) {
const nearVectors = [];
const searchRadiusFactor = 1.5;
for (let x = 0; x <= gridSize; x++) {
const xCoord = x * gridStep;
for (let y = 0; y <= gridSize; y++) {
const yCoord = y * gridStep;
const start = { x: xCoord, y: yCoord };
if (isDotInCircle(start, dot, gridStep * searchRadiusFactor)) {
nearVectors.push({
start,
vector: vectors[x][y]
});
}
}
}
if (nearVectors.length === 0) {
return;
}
let nextDot = { ...dot };
nearVectors.forEach(vector => {
const distance = getDistance(dot, vector.start);
nextDot = addVectors(
nextDot,
multiplyVector(vector.vector, searchRadiusFactor - distance)
);
});
drawDot(nextDot);
line.push([nextDot.x, nextDot.y]);
dotsDrawn++;
if (
nextDot.x > size * 1.1 ||
nextDot.y > size * 1.1 ||
nextDot.x < size * -0.1 ||
nextDot.y < size * -0.1
) {
return;
}
if (dotsDrawn < maxDots) {
drawNextDot(nextDot);
}
}
function isDotInCircle(dot, circleCenter, radius) {
const x = dot.x - circleCenter.x;
const y = dot.y - circleCenter.y;
return x * x + y * y < radius * radius;
}
function getRandomColor() {
const goldenRatio = 0.618033988749895 * 360;
let h = Math.random() * 360;
h += goldenRatio;
h %= 360;
return `hsl(${ parseInt(h, 10) }, 70%, 60%)`;
}
const maxDots = 100;
let dotsDrawn = 0;
let line = [];
svg.addEventListener("click", e => {
const rect = svg.getBoundingClientRect();
const x = e.pageX - (rect.left + window.pageXOffset);
const y = e.pageY - (rect.top + window.pageYOffset);
const factor = rect.width / size;
dotsDrawn = 0;
line = [];
const start = {
x: x / factor,
y: y / factor
};
line.push([start.x, start.y]);
drawDot(start);
drawNextDot(start);
svg.innerHTML += svgPath(line, bezierCommand);
/*`
<path
class="generated-line"
fill="none"
stroke="${ getRandomColor() }"
d="${line.join(" ")}"
stroke-width="${(gridStep / 30).toFixed(2)}"
/>`;
*/
});
function clearLines() {
document.querySelectorAll(".generated-line, .generated-dot").forEach(line => {
svg.removeChild(line);
});
}
function init() {
svg.innerHTML = `
<marker
id="arrow"
viewBox="0 0 10 10"
refX="5"
refY="5"
markerWidth="${gridStep / 5}"
markerHeight="${gridStep / 5}"
orient="auto-start-reverse"
stroke-linecap="round"
>
<path d="M 0 0 L 10 5 L 0 10 Z" />
</marker>
`;
for (let x = 0; x <= gridSize; x++) {
vectors[x] = [];
const coord = x * gridStep;
svg.innerHTML += `
<path
class="coordinate-line"
d="M ${0} ${coord} L ${size} ${coord}"
stroke-width="${gridStep / 100}"
/>
<path
class="coordinate-line"
d="M ${coord} ${0} L ${coord} ${size}"
stroke-width="${gridStep / 100}"
/>
`;
for (let y = 0; y <= gridSize; y++) {
vectors[x][y] = getRandomVector();
}
}
vectors.forEach((row, x) => {
row.forEach((vector, y) => {
const xCoord = x * gridStep;
const yCoord = y * gridStep;
svg.innerHTML += `
<path
class="vector"
d="M ${xCoord} ${yCoord} L ${(xCoord + vector.x).toFixed(2)} ${ (yCoord + vector.y).toFixed(2) }"
marker-end="url(#arrow)"
stroke-width="${(gridStep / 30).toFixed(2)}"
stroke-linecap="round"
/>
`;
});
});
}
init();
// The smoothing ratio
const smoothing = 0.15;
// Properties of a line
// I: - pointA (array) [x,y]: coordinates
// - pointB (array) [x,y]: coordinates
// O: - (object) { length: l, angle: a }: properties of the line
const lineFn = (pointA, pointB) => {
const lengthX = pointB[0] - pointA[0]
const lengthY = pointB[1] - pointA[1]
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX)
}
}
// Position of a control point
// I: - current (array) [x, y]: current point coordinates
// - previous (array) [x, y]: previous point coordinates
// - next (array) [x, y]: next point coordinates
// - reverse (boolean, optional): sets the direction
// O: - (array) [x,y]: a tuple of coordinates
const controlPoint = (current, previous, next, reverse) => {
// When 'current' is the first or last point of the array
// 'previous' or 'next' don't exist.
// Replace with 'current'
const p = previous || current
const n = next || current
// Properties of the opposed-line
const o = lineFn(p, n)
// If is end-control-point, add PI to the angle to go backward
const angle = o.angle + (reverse ? Math.PI : 0)
const length = o.length * smoothing
// The control point position is relative to the current point
const x = current[0] + Math.cos(angle) * length
const y = current[1] + Math.sin(angle) * length
return [x, y]
}
// Create the bezier curve command
// I: - point (array) [x,y]: current point coordinates
// - i (integer): index of 'point' in the array 'a'
// - a (array): complete array of points coordinates
// O: - (string) 'C x2,y2 x1,y1 x,y': SVG cubic bezier C command
const bezierCommand = (point, i, a) => {
// start control point
const cps = controlPoint(a[i - 1], a[i - 2], point)
// end control point
const cpe = controlPoint(point, a[i - 1], a[i + 1], true)
return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`
}
// Render the svg <path> element
// I: - points (array): points coordinates
// - command (function)
// I: - point (array) [x,y]: current point coordinates
// - i (integer): index of 'point' in the array 'a'
// - a (array): complete array of points coordinates
// O: - (string) a svg path command
// O: - (string): a Svg <path> element
const svgPath = (points, command) => {
if (drawDotsLines.checked) {
// build the d attributes by looping over the points
const d = points.reduce((acc, point, i, a) => i === 0
? `M ${point[0]},${point[1]}`
: `${acc} ${command(point, i, a)}`
, '')
return `<path class="generated-line" d="${d}" fill="none" stroke="${ getRandomColor() }" />`
}
}
/*
for (let x = 0; x < gridSize; x++) {
const coord = x * gridStep;
svg.innerHTML += `
<path
class="coordinate-line"
d="M ${ 0 } ${ coord } L ${ coord } ${ size }"
stroke-width="${ gridStep / 50 }"
/>
<path
class="coordinate-line"
d="M ${ coord } ${ 0 } L ${ size } ${ coord }"
stroke-width="${ gridStep / 50 }"
/>
`;
}
*/
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.