Anime JS animate incrementally to a specific point along a path - javascript

https://codepen.io/jesserosenfield/pen/LYNGRXV
var path = anime.path('#prog-svg path'),
pathEl = document.querySelectorAll('#prog-svg path')[0],
mylength = pathEl.getTotalLength(),
mypt1 = pathEl.getPointAtLength(mylength * .10),
mypt2 = pathEl.getPointAtLength(mylength * .25);
var motionPath = anime({
targets: '.prog-circ',
translateX: path('x'),
translateY: path('y'),
rotate: path('angle'),
easing: 'easeInOutCirc',
duration: 5000,
direction: 'alternate',
autoplay: false,
elasticity: 200,
loop: false,
update: function(anim){
console.log(path('x'));
}
});
motionPath.seek(1210);
motionPath.play();
This code does what I want it to do in the broad scheme of things, but I have a more specific use case.
I'm using this SVG as a progress bar on a form:
When the user completes step #1 of the form, I want the circle to animate from point A to point B. When the user completes step #2 of the form, I want the circle to animate from point B to point C... and so on.
While motionpath.seek() gets me to the correct point along the path, it sets the circle there with no animation– is there an equivalent function to seek() that will get ANIMATE the circle rather than just set it?
Furthermore I attempted to use getTotalLength() and getPointAtLength() to try and animate like so:
var motionPath = anime({
targets: '.prog-circ',
translateX: [mypt1.x, mypt2.x],
translateY: [mypt1.y, mypt2.y],
but that did not animate the circle along the path.
Any help much appreciated. Thanks!

With one long path I think it's hard to support moving between points since you need to track current progress and convert it to actual length depending on easing function.
I'd split your <path/> into 3 pieces, generate timeline for animation between those 3 pieces and then easily manipulate moving circle back and forth.
Here's an example of how it can be done:
const svg = document.getElementById('prog-svg');
const pathEl = document.querySelector('#prog-svg path');
const totalLength = pathEl.getTotalLength();
const points = [['A', 10], ['B', 25], ['C', 75], ['D', 90]];
function splitPath() {
const interval = 3;
const toLen = percentage => percentage * totalLength / 100;
const paths = [];
for (let i = 0; i < points.length; i++) {
const from = toLen(points[i][1]);
for (let j = i + 1; j < points.length; j++) {
const to = toLen(points[j][1]);
const segments = [];
for (let k = from; k <= to; k += interval) {
const { x, y } = pathEl.getPointAtLength(k);
segments.push([x, y]);
}
paths.push({
segments, path: `${i}-${j}`
});
}
}
paths.forEach(subPath => {
const subPathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
subPathEl.setAttribute('class', `st0 st0--hidden`);
subPathEl.setAttribute('d', `M ${subPath.segments.map(([x, y]) => `${x},${y}`).join(' ')}`);
svg.appendChild(subPathEl);
subPath.el = subPathEl;
});
return paths;
}
const subPaths = splitPath();
function addPoint(name, progress) {
const point = pathEl.getPointAtLength(totalLength * progress / 100);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('fill', '#fff');
text.setAttribute('font-size', '1.6em');
text.textContent = name;
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('r', '30');
circle.setAttribute('fill', '#000');
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('transform', `translate(${point.x},${point.y})`);
g.appendChild(circle);
g.appendChild(text);
svg.appendChild(g);
// center text
const textBB = text.getBBox();
const centerX = textBB.width / 2;
const centerY = textBB.height / 4;
text.setAttribute('transform', `translate(${-centerX},${centerY})`);
return circle;
}
points.forEach(([name, progress]) => addPoint(name, progress));
const progressCircle = document.querySelector('.prog-circ');
progressCircle.style.display = 'block';
const animations = subPaths.map(subPath => {
const animePath = anime.path(subPath.el);
return anime({
targets: progressCircle,
easing: 'easeInOutCirc',
autoplay: false,
duration: 1000,
translateX: animePath('x'),
translateY: animePath('y'),
rotate: animePath('angle'),
});
});
// move circle to the first point
animations[0].reset();
let currentStep = 0;
function moveTo(step) {
if (step < 0 || step > animations.length) return;
const delta = step - currentStep;
const path = delta > 0 ? `${currentStep}-${step}` : `${step}-${currentStep}`;
const animationIndex = subPaths.findIndex(subPath => subPath.path === path);
const animationToPlay = animations[animationIndex];
if (delta < 0 && !animationToPlay.reversed) {
animationToPlay.reverse();
}
if (delta > 0 && animationToPlay.reversed) {
animationToPlay.reverse();
}
animationToPlay.reset();
animationToPlay.play();
currentStep = step;
pagination.selectedIndex = step;
}
const btnPrev = document.getElementById('btn-prev');
const btnNext = document.getElementById('btn-next');
const pagination = document.getElementById('pagination');
btnPrev.addEventListener('click', () => moveTo(currentStep - 1));
btnNext.addEventListener('click', () => moveTo(currentStep + 1));
pagination.addEventListener('change', (e) => moveTo(+e.target.value));
body {
margin: 0;
}
.st0 {
fill: none;
stroke: #000000;
stroke-width: 5;
stroke-linecap: round;
stroke-miterlimit: 160;
stroke-dasharray: 28;
}
.st0--hidden {
stroke: none;
}
.prog-circ {
display: none;
position: absolute;
border-radius: 100%;
height: 30px;
width: 30px;
top: -15px;
left: -15px;
background: #ccc;
opacity: .7;
}
.form-actions {
margin-top: 2em;
display: flex;
justify-content: center;
}
#pagination,
.form-actions button + button {
margin-left: 2em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.0/anime.min.js"></script>
<svg id="prog-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1919.1 155.4">
<g>
<path class="st0" d="M4,84.1c0,0,58.8-57.1,235.1,17.9s348.1,18.9,470.2-44.6C800.6,9.7,869.6-2,953.5,6.6c0,0,19,4.1,38.6,14.4
c20.7,10.9,40.7,40.6,40.7,65.6c0,40.2-29.5,64.8-69.7,64.8s-70.1-29.2-70.1-69.4c0-32.3,31.2-59.6,61.8-61.8
c67.2-4.7,103.5-46.8,375.6,70.1c164.9,70.8,220.1-1.1,371.1-11.7c120.5-8.4,213.7,28.6,213.7,28.6"/>
</g>
</svg>
<div class="prog-circ"></div>
<div class="form-actions">
<button id="btn-prev">Prev</button>
<button id="btn-next">Next</button>
<select id="pagination">
<option value="0">A</option>
<option value="1">B</option>
<option value="2">C</option>
<option value="3">D</option>
</select>
</div>

Related

Spinner using spin.js Not Spinning

Good day Guys,
I am trying to use a spinner that shows on the entire page when i click on Submit button. The below are the codes snippets.
This is the JS code
<script type="text/javascript"
src="#Url.Content("~/Scripts/spin.js")"></script>
<script type="text/javascript">
$(function () {
$("#searchbtn").click(function () {
$("#loading").fadeIn();
var opts = {
lines: 12, // The number of lines to draw
length: 7, // The length of each line
width: 4, // The line thickness
radius: 10, // The radius of the inner circle
color: '#000', // #rgb or #rrggbb
speed: 1, // Rounds per second
trail: 60, // Afterglow percentage
shadow: false, // Whether to render a shadow
hwaccel: false // Whether to use hardware acceleration
};
var target = document.getElementById('loading');
//var spinner = new Spinner(opts).spin(target);
var spinner = new Spin.Spinner(opts).spin(target);
});
});
This is the CSS below
#loading {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.8);
z-index: 1000;
}
#loadingcontent {
display: table;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
#loadingspinner {
display: table-cell;
vertical-align: middle;
width: 100%;
text-align: center;
font-size: larger;
padding-top: 80px;
}
Below is the DIV that holds the searching.
<div id="loading">
<div id="loadingcontent">
<p id="loadingspinner">
Searching things...
</p>
</div>
</div>
<div class="col-md-12">
<p>
#using (Html.BeginForm("AllLoanProcessed", "Transactions", new { area = "Transactions" }, FormMethod.Get))
{
<b>Search By:</b>
#Html.RadioButton("searchBy", "Account_Number", true) <text>Account Number</text>
#Html.RadioButton("searchBy", "Surname") <text> Surname </text> <br />
#Html.TextBox("search", null, new { placeholder = "Search Value", #class = "form-control" })
<br />
<input type="submit" value="Search" id="searchbtn" class="btn btn-primary btn-block" />
}
</p>
</div>
The issue is that, when i click on the search button, The spin does not load.
Am I missing something? OR any one has any other spinner method that works that will cover the whole page when running.
EDIT:
The below is the spin.js file content.
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function (t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var defaults = {
lines: 12,
length: 7,
width: 5,
radius: 10,
scale: 1.0,
corners: 1,
color: '#000',
fadeColor: 'transparent',
animation: 'spinner-line-fade-default',
rotate: 0,
direction: 1,
speed: 1,
zIndex: 2e9,
className: 'spinner',
top: '50%',
left: '50%',
shadow: '0 0 1px transparent',
position: 'absolute',
};
var Spinner = /** #class */ (function () {
function Spinner(opts) {
if (opts === void 0) { opts = {}; }
this.opts = __assign(__assign({}, defaults), opts);
}
/**
* Adds the spinner to the given target element. If this instance is already
* spinning, it is automatically removed from its previous target by calling
* stop() internally.
*/
Spinner.prototype.spin = function (target) {
this.stop();
this.el = document.createElement('div');
this.el.className = this.opts.className;
this.el.setAttribute('role', 'progressbar');
css(this.el, {
position: this.opts.position,
width: 0,
zIndex: this.opts.zIndex,
left: this.opts.left,
top: this.opts.top,
transform: "scale(" + this.opts.scale + ")",
});
if (target) {
target.insertBefore(this.el, target.firstChild || null);
}
drawLines(this.el, this.opts);
return this;
};
/**
* Stops and removes the Spinner.
* Stopped spinners may be reused by calling spin() again.
*/
Spinner.prototype.stop = function () {
if (this.el) {
if (typeof requestAnimationFrame !== 'undefined') {
cancelAnimationFrame(this.animateId);
}
else {
clearTimeout(this.animateId);
}
if (this.el.parentNode) {
this.el.parentNode.removeChild(this.el);
}
this.el = undefined;
}
return this;
};
return Spinner;
}());
export { Spinner };
/**
* Sets multiple style properties at once.
*/
function css(el, props) {
for (var prop in props) {
el.style[prop] = props[prop];
}
return el;
}
/**
* Returns the line color from the given string or array.
*/
function getColor(color, idx) {
return typeof color == 'string' ? color : color[idx % color.length];
}
/**
* Internal method that draws the individual lines.
*/
function drawLines(el, opts) {
var borderRadius = (Math.round(opts.corners * opts.width * 500) / 1000) +
'px';
var shadow = 'none';
if (opts.shadow === true) {
shadow = '0 2px 4px #000'; // default shadow
}
else if (typeof opts.shadow === 'string') {
shadow = opts.shadow;
}
var shadows = parseBoxShadow(shadow);
for (var i = 0; i < opts.lines; i++) {
var degrees = ~~(360 / opts.lines * i + opts.rotate);
var backgroundLine = css(document.createElement('div'), {
position: 'absolute',
top: -opts.width / 2 + "px",
width: (opts.length + opts.width) + 'px',
height: opts.width + 'px',
background: getColor(opts.fadeColor, i),
borderRadius: borderRadius,
transformOrigin: 'left',
transform: "rotate(" + degrees + "deg) translateX(" + opts.radius +
"px)",
});
var delay = i * opts.direction / opts.lines / opts.speed;
delay -= 1 / opts.speed; // so initial animation state will include trail
var line = css(document.createElement('div'), {
width: '100%',
height: '100%',
background: getColor(opts.color, i),
borderRadius: borderRadius,
boxShadow: normalizeShadow(shadows, degrees),
animation: 1 / opts.speed + "s linear " + delay + "s infinite " +
opts.animation,
});
backgroundLine.appendChild(line);
el.appendChild(backgroundLine);
}
}
function parseBoxShadow(boxShadow) {
var regex = /^\s*([a-zA-Z]+\s+)?(-?\d+(\.\d+)?)([a-zA-Z]*)\s+(-?\d+(\.\d+)?)
([a-zA-Z]*)(.*)$/;
var shadows = [];
for (var _i = 0, _a = boxShadow.split(','); _i < _a.length; _i++) {
var shadow = _a[_i];
var matches = shadow.match(regex);
if (matches === null) {
continue; // invalid syntax
}
var x = +matches[2];
var y = +matches[5];
var xUnits = matches[4];
var yUnits = matches[7];
if (x === 0 && !xUnits) {
xUnits = yUnits;
}
if (y === 0 && !yUnits) {
yUnits = xUnits;
}
if (xUnits !== yUnits) {
continue; // units must match to use as coordinates
}
shadows.push({
prefix: matches[1] || '',
x: x,
y: y,
xUnits: xUnits,
yUnits: yUnits,
end: matches[8],
});
}
return shadows;
}
/**
* Modify box-shadow x/y offsets to counteract rotation
*/
function normalizeShadow(shadows, degrees) {
var normalized = [];
for (var _i = 0, shadows_1 = shadows; _i < shadows_1.length; _i++) {
var shadow = shadows_1[_i];
var xy = convertOffset(shadow.x, shadow.y, degrees);
normalized.push(shadow.prefix + xy[0] + shadow.xUnits + ' ' + xy[1] +
shadow.yUnits + shadow.end);
}
return normalized.join(', ');
}
function convertOffset(x, y, degrees) {
var radians = degrees * Math.PI / 180;
var sin = Math.sin(radians);
var cos = Math.cos(radians);
return [
Math.round((x * cos + y * sin) * 1000) / 1000,
Math.round((-x * sin + y * cos) * 1000) / 1000,
];
}

Animation creates a div that moves my page content?

I have the following site on which I tried implementing a very nice looking animation. I don't know why but it keeps creating white space around my page and if I also add a button (as I did in the fiddle) it just goes crazy. What is the solution to this?
FIDDLE: https://jsfiddle.net/7suL84my/
CODE OF ANIMATION:
// Some random colors
const colors = ["#3CC157", "#2AA7FF", "#1B1B1B", "#FCBC0F", "#F85F36"];
const numBalls = 50;
const balls = [];
for (let i = 0; i < numBalls; i++) {
let ball = document.createElement("div");
ball.classList.add("ball");
ball.style.background = colors[Math.floor(Math.random() * colors.length)];
ball.style.left = `${Math.floor(Math.random() * 100)}vw`;
ball.style.top = `${Math.floor(Math.random() * 100)}vh`;
ball.style.transform = `scale(${Math.random()})`;
ball.style.width = `${Math.random()}em`;
ball.style.height = ball.style.width;
balls.push(ball);
document.body.append(ball);
}
// Keyframes
balls.forEach((el, i, ra) => {
let to = {
x: Math.random() * (i % 2 === 0 ? -11 : 11),
y: Math.random() * 12
};
let anim = el.animate(
[
{ transform: "translate(0, 0)" },
{ transform: `translate(${to.x}rem, ${to.y}rem)` }
],
{
duration: (Math.random() + 1) * 2000, // random duration
direction: "alternate",
fill: "both",
iterations: Infinity,
easing: "ease-in-out"
}
);
});
From your link on the css file change this :
#main{
background-color:black;
color:white;
width:98%;
height:98%;
align-content: center;
}
to this:
#main{
background-color:black;
color:white;
width:100%;
height:100%;
align-content: center;
}

Restricting Javascript based Background Animation to some Particular Div

I have been trying to use an Animated Background, using Javascript I found on this link : https://codepen.io/nashvail/pen/wpGgXO
It's working fine. But the problem is that once implemented, it's all over the screen.
I want the animations to be in some particular DIV element of my web page.
Help me Out.
// Some random colors
const colors = ["#3CC157", "#2AA7FF", "#1B1B1B", "#FCBC0F", "#F85F36"];
const numBalls = 50;
const balls = [];
for (let i = 0; i < numBalls; i++) {
let ball = document.createElement("div");
ball.classList.add("ball");
ball.style.background = colors[Math.floor(Math.random() * colors.length)];
ball.style.left = `${Math.floor(Math.random() * 100)}vw`;
ball.style.top = `${Math.floor(Math.random() * 100)}vh`;
ball.style.transform = `scale(${Math.random()})`;
ball.style.width = `${Math.random()}em`;
ball.style.height = ball.style.width;
balls.push(ball);
document.body.append(ball);
}
// Keyframes
balls.forEach((el, i, ra) => {
let to = {
x: Math.random() * (i % 2 === 0 ? -11 : 11),
y: Math.random() * 12
};
let anim = el.animate(
[
{ transform: "translate(0, 0)" },
{ transform: `translate(${to.x}rem, ${to.y}rem)` }
],
{
duration: (Math.random() + 1) * 2000, // random duration
direction: "alternate",
fill: "both",
iterations: Infinity,
easing: "ease-in-out"
}
);
});
.ball {
position: absolute;
border-radius: 100%;
opacity: 0.7;
}
<div class="ball">
</div>
Like this?
// Some random colors
const colors = ["#3CC157", "#2AA7FF", "#1B1B1B", "#FCBC0F", "#F85F36"];
const numBalls = 50;
const balls = [];
for (let i = 0; i < numBalls; i++) {
let ball = document.createElement("div");
ball.classList.add("ball");
ball.style.background = colors[Math.floor(Math.random() * colors.length)];
ball.style.left = `${Math.floor(Math.random() * 100)}%`;
ball.style.top = `${Math.floor(Math.random() * 100)}%`;
ball.style.transform = `scale(${Math.random()})`;
ball.style.width = `${Math.random()}em`;
ball.style.height = ball.style.width;
balls.push(ball);
document.getElementById("box").append(ball);
}
// Keyframes
balls.forEach((el, i, ra) => {
let to = {
x: Math.random() * (i % 2 === 0 ? -11 : 11),
y: Math.random() * 12
};
let anim = el.animate(
[
{ transform: "translate(0, 0)" },
{ transform: `translate(${to.x}rem, ${to.y}rem)` }
],
{
duration: (Math.random() + 1) * 2000, // random duration
direction: "alternate",
fill: "both",
iterations: Infinity,
easing: "ease-in-out"
}
);
});
.ball {
position: absolute;
border-radius: 100%;
opacity: 0.7;
}
#box{
width: 300px;
height: 300px;
border: 1px solid red;
position: relative;
overflow: hidden;
}
<div id="box"></div>
The issue is, you are appending balls to the body. That's why they are appearing all over the screen. You have to make a container with some width and height and append the created balls to that container only:
// Some random colors
const colors = ["#3CC157", "#2AA7FF", "#1B1B1B", "#FCBC0F", "#F85F36"];
const numBalls = 50;
const balls = [];
for (let i = 0; i < numBalls; i++) {
let ball = document.createElement("div");
ball.classList.add("ball");
ball.style.background = colors[Math.floor(Math.random() * colors.length)];
ball.style.left = `${Math.floor(Math.random() * 100)}vw`;
ball.style.top = `${Math.floor(Math.random() * 100)}vh`;
ball.style.transform = `scale(${Math.random()})`;
ball.style.width = `${Math.random()}em`;
ball.style.height = ball.style.width;
balls.push(ball);
document.querySelector('.ballContainer').append(ball);
}
// Keyframes
balls.forEach((el, i, ra) => {
let to = {
x: Math.random() * (i % 2 === 0 ? -11 : 11),
y: Math.random() * 12
};
let anim = el.animate(
[
{ transform: "translate(0, 0)" },
{ transform: `translate(${to.x}rem, ${to.y}rem)` }
],
{
duration: (Math.random() + 1) * 2000, // random duration
direction: "alternate",
fill: "both",
iterations: Infinity,
easing: "ease-in-out"
}
);
});
.ball {
position: absolute;
border-radius: 100%;
opacity: 0.7;
}
.ballContainer{
width: 350px;
height: 175px;
border: 1px solid gray;
position: relative;
overflow: hidden;
background-color: lightgray;
}
<div class="ballContainer">
</div>
The balls run a random path. That means some of balls will go out of bounds (your div).
You can add CSS to hide the scroll bars.
html * {
overflow: hidden;
}

Retrieve an element inside an object collection

I'm making board game, and I want that when I click in one of the places I turn them red. I have this array of divs, but I don't know how to retrieve the element given the number to turn it red. How can I do that, I'm trying to use .element but it's not working
var number = 3;
const board = [];
const boardWidth = boardHeight = 10;
(function() {
const boardElement = document.getElementById('board');
for (var y = 0; y < boardHeight; ++y) {
var row = [];
for (var x = 0; x < boardWidth; ++x) {
var cell = {};
cell.element = document.createElement('div');
boardElement.appendChild(cell.element);
row.push(cell);
}
board.push(row);
}
painting();
})();
function painting() {
board[number][number].element.style.backgroundcolor = 'red';
}
#board {
width: calc(10 * 30px);
margin: auto;
}
#board div {
background-color: black;
border: 1px solid white;
box-sizing: border-box;
float: left;
width: 30px;
height: 30px;
}
<div id="board"></div>
Your code look very confusing. board is an element and you are using it as an array.
Next come some code. I hope this is what you need:
let cellW = 100;
let cellH = 100;
function init(){
let boardArray = [];
let bStyle = window.getComputedStyle(board, null);
let bWidth = parseInt(bStyle.getPropertyValue("width"));
let bHeight = parseInt(bStyle.getPropertyValue("height"));
for (let y = 0; y < bHeight; y+=cellH) {
let row = [];
for (let x = 0; x < bWidth; x+=cellW) {
let cell = {};
cell.element = document.createElement('div');
cell.element.style.width = cellW +"px";
cell.element.style.height = cellH +"px";
board.appendChild(cell.element);
row.push(cell);
}
boardArray.push(row);
}
}
init();
let cells = Array.from(document.querySelectorAll("#board div"));
cells.map( cell => {
cell.addEventListener("click", e =>{
cell.style.background = "red"
})
})
#board{width: 1000px; height:500px; display:flex;flex-wrap:wrap;}
#board div{outline:1px solid;}
<div id="board"></div>
UPDATE:
I understand that you need to make the 4-th cell in the cells array red:
var number = 3;
const board = [];
const boardWidth = 10, boardHeight = 10;
function init() {
const boardElement = document.getElementById('board');
for (var y = 0; y < boardHeight; ++y) {
var row = [];
for (var x = 0; x < boardWidth; ++x) {
var cell = {};
cell.element = document.createElement('div');
boardElement.appendChild(cell.element);
row.push(cell);
}
board.push(row);
}
board[number][number].element.style.background = "red"
}
window.addEventListener("load", init);
#board {
width: calc(10 * 30px);
margin: auto;
}
#board div {
background-color: black;
border: 1px solid white;
box-sizing: border-box;
float: left;
width: 30px;
height: 30px;
}
<div id="board"></div>
I've addressed all of your issues but thought you might be interested in making things configurable. I reworked your code to make things config driven.
This will:
Take a config (or not) and merge it with a default config.
Build your board dynamically based on config values v. setting the dimensions via CSS
Allows for cell toggle (select/unselect) and matric position assignments
document.addEventListener('DOMContentLoaded', function(){
const extend = function(target, config, defaults){
defaults && extend(target, defaults);
if(target && config && typeof(config) === 'object'){
for(const i in config){
target[i] = config[i];
}
}
return target;
};
function Game(config){
const defaultConfig = {
boardElement: '#board',
// if a single digit is passed it will be duplicated for pos y 3 => 3,3
startPosition: 3,
cellSize: 30,
boardWidth: 10,
boardHeight: 10
};
// merge the default and user-defined config into a new config
this.config = extend({}, config || {}, defaultConfig);
// cache ref to your board element
this.boardElement = document.querySelector(this.config.boardElement);
// stores our collection of board items
this.board = [];
// draw the board
this.draw();
// set initial marker
if(this.config.startPosition){
if(this.config.startPosition instanceof Array){
this.paint.apply(this, this.config.startPosition);
}else{
this.paint(this.config.startPosition);
}
}
return this;
}
extend(Game.prototype, {
draw(){
for (let y = 0; y < this.config.boardHeight; ++y) {
const row = [];
for (var x = 0; x < this.config.boardWidth; ++x) {
const element = document.createElement('div');
const cell = {
element,
position: {
x: x + 1,
y: y + 1
}
};
// set cell width and height
element.style.height = element.style.width = `${this.config.cellSize}px`;
// handle selecting/unselecting cells
element.addEventListener('click', () => this.paint(cell.position.x, cell.position.y));
this.boardElement.appendChild(cell.element);
row.push(cell);
}
this.board.push(row);
}
// set board width and height
this.boardElement.style.width = `${this.config.boardWidth * this.config.cellSize}px`;
},
paint(x, y){
if(y === undefined){
y = x;
}
const element = this.board[y-1][x-1].element;
if(element){
const isSelcted = element.style.backgroundColor === 'red';
element.style.backgroundColor = isSelcted ? 'black' : 'red';
}
}
});
new Game({
startPosition: [5,4],
boardWidth: 8,
boardHeight: 8
});
});
#board {
margin: auto;
}
#board div {
background-color: black;
border: 1px solid white;
box-sizing: border-box;
float: left;
}
<div id="board"></div>

SVG smooth freehand drawing

I implemented a freehand drawing of a path using native JS. But as expected path edges are little aggressive and not smooth. So I have an option of using simplifyJS to simplify points and then redraw path. But like here, instead of smoothening after drawing, I am trying to find simplified edges while drawing
Here is my code:
var x0, y0;
var dragstart = function(event) {
var that = this;
var pos = coordinates(event);
x0 = pos.x;
y0 = pos.y;
that.points = [];
};
var dragging = function(event) {
var that = this;
var xy = coordinates(event);
var points = that.points;
var x1 = xy.x, y1 = xy.y, dx = x1 - x0, dy = y1 - y0;
if (dx * dx + dy * dy > 100) {
xy = {
x: x0 = x1,
y: y0 = y1
};
} else {
xy = {
x: x1,
y: y1
};
}
points.push(xy);
};
But it is not working as in the link added above. Still edges are not good. Please help.
The following code snippet makes the curve smoother by calculating the average of the last mouse positions. The level of smoothing depends on the size of the buffer in which these values are kept. You can experiment with the different buffer sizes offered in the dropdown list. The behavior with a 12 point buffer is somewhat similar to the Mike Bostock's code snippet that you refer to in the question.
More sophisticated techniques could be implemented to get the smoothed point from the positions stored in the buffer (weighted average, linear regression, cubic spline smoothing, etc.) but this simple average method may be sufficiently accurate for your needs.
var strokeWidth = 2;
var bufferSize;
var svgElement = document.getElementById("svgElement");
var rect = svgElement.getBoundingClientRect();
var path = null;
var strPath;
var buffer = []; // Contains the last positions of the mouse cursor
svgElement.addEventListener("mousedown", function (e) {
bufferSize = document.getElementById("cmbBufferSize").value;
path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute("fill", "none");
path.setAttribute("stroke", "#000");
path.setAttribute("stroke-width", strokeWidth);
buffer = [];
var pt = getMousePosition(e);
appendToBuffer(pt);
strPath = "M" + pt.x + " " + pt.y;
path.setAttribute("d", strPath);
svgElement.appendChild(path);
});
svgElement.addEventListener("mousemove", function (e) {
if (path) {
appendToBuffer(getMousePosition(e));
updateSvgPath();
}
});
svgElement.addEventListener("mouseup", function () {
if (path) {
path = null;
}
});
var getMousePosition = function (e) {
return {
x: e.pageX - rect.left,
y: e.pageY - rect.top
}
};
var appendToBuffer = function (pt) {
buffer.push(pt);
while (buffer.length > bufferSize) {
buffer.shift();
}
};
// Calculate the average point, starting at offset in the buffer
var getAveragePoint = function (offset) {
var len = buffer.length;
if (len % 2 === 1 || len >= bufferSize) {
var totalX = 0;
var totalY = 0;
var pt, i;
var count = 0;
for (i = offset; i < len; i++) {
count++;
pt = buffer[i];
totalX += pt.x;
totalY += pt.y;
}
return {
x: totalX / count,
y: totalY / count
}
}
return null;
};
var updateSvgPath = function () {
var pt = getAveragePoint(0);
if (pt) {
// Get the smoothed part of the path that will not change
strPath += " L" + pt.x + " " + pt.y;
// Get the last part of the path (close to the current mouse position)
// This part will change if the mouse moves again
var tmpPath = "";
for (var offset = 2; offset < buffer.length; offset += 2) {
pt = getAveragePoint(offset);
tmpPath += " L" + pt.x + " " + pt.y;
}
// Set the complete current path coordinates
path.setAttribute("d", strPath + tmpPath);
}
};
html, body
{
padding: 0px;
margin: 0px;
}
#svgElement
{
border: 1px solid;
margin-top: 4px;
margin-left: 4px;
cursor: default;
}
#divSmoothingFactor
{
position: absolute;
left: 14px;
top: 12px;
}
<div id="divSmoothingFactor">
<label for="cmbBufferSize">Buffer size:</label>
<select id="cmbBufferSize">
<option value="1">1 - No smoothing</option>
<option value="4">4 - Sharp curves</option>
<option value="8" selected="selected">8 - Smooth curves</option>
<option value="12">12 - Very smooth curves</option>
<option value="16">16 - Super smooth curves</option>
<option value="20">20 - Hyper smooth curves</option>
</select>
</div>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="svgElement" x="0px" y="0px" width="600px" height="400px" viewBox="0 0 600 400" enable-background="new 0 0 600 400" xml:space="preserve">
Quadtratic Bézier polyline smoothing
#ConnorsFan solution works great and is probably providing a better rendering performance and more responsive drawing experience.
In case you need a more compact svg output (in terms of markup size) quadratic smoothing might be interesting.
E.g. if you need to export the drawings in an efficient way.
Simplified example: polyline smoothing
Green dots show the original polyline coordinates (in x/y pairs).
Purple points represent interpolated middle coordinates – simply calculated like so:
[(x1+x2)/2, (y1+y2)/2].
The original coordinates (highlighted green) become quadratic bézier control points
whereas the interpolated middle points will be the end points.
let points = [{
x: 0,
y: 10
},
{
x: 10,
y: 20
},
{
x: 20,
y: 10
},
{
x: 30,
y: 20
},
{
x: 40,
y: 10
}
];
path.setAttribute("d", smoothQuadratic(points));
function smoothQuadratic(points) {
// set M/starting point
let [Mx, My] = [points[0].x, points[0].y];
let d = `M ${Mx} ${My}`;
renderPoint(svg, [Mx, My], "green", "1");
// split 1st line segment
let [x1, y1] = [points[1].x, points[1].y];
let [xM, yM] = [(Mx + x1) / 2, (My + y1) / 2];
d += `L ${xM} ${yM}`;
renderPoint(svg, [xM, yM], "purple", "1");
for (let i = 1; i < points.length; i += 1) {
let [x, y] = [points[i].x, points[i].y];
// calculate mid point between current and next coordinate
let [xN, yN] = points[i + 1] ? [points[i + 1].x, points[i + 1].y] : [x, y];
let [xM, yM] = [(x + xN) / 2, (y + yN) / 2];
// add quadratic curve:
d += `Q${x} ${y} ${xM} ${yM}`;
renderPoint(svg, [xM, yM], "purple", "1");
renderPoint(svg, [x, y], "green", "1");
}
return d;
}
pathRel.setAttribute("d", smoothQuadraticRelative(points));
function smoothQuadraticRelative(points, skip = 0, decimals = 3) {
let pointsL = points.length;
let even = pointsL - skip - (1 % 2) === 0;
// set M/starting point
let type = "M";
let values = [points[0].x, points[0].y];
let [Mx, My] = values.map((val) => {
return +val.toFixed(decimals);
});
let dRel = `${type}${Mx} ${My}`;
// offsets for relative commands
let xO = Mx;
let yO = My;
// split 1st line segment
let [x1, y1] = [points[1].x, points[1].y];
let [xM, yM] = [(Mx + x1) / 2, (My + y1) / 2];
let [xMR, yMR] = [xM - xO, yM - yO].map((val) => {
return +val.toFixed(decimals);
});
dRel += `l${xMR} ${yMR}`;
xO += xMR;
yO += yMR;
for (let i = 1; i < points.length; i += 1 + skip) {
// control point
let [x, y] = [points[i].x, points[i].y];
let [xR, yR] = [x - xO, y - yO];
// next point
let [xN, yN] = points[i + 1 + skip] ?
[points[i + 1 + skip].x, points[i + 1 + skip].y] :
[points[pointsL - 1].x, points[pointsL - 1].y];
let [xNR, yNR] = [xN - xO, yN - yO];
// mid point
let [xM, yM] = [(x + xN) / 2, (y + yN) / 2];
let [xMR, yMR] = [(xR + xNR) / 2, (yR + yNR) / 2];
type = "q";
values = [xR, yR, xMR, yMR];
// switch to t command
if (i > 1) {
type = "t";
values = [xMR, yMR];
}
dRel += `${type}${values
.map((val) => {
return +val.toFixed(decimals);
})
.join(" ")} `;
xO += xMR;
yO += yMR;
}
// add last line if odd number of segments
if (!even) {
values = [points[pointsL - 1].x - xO, points[pointsL - 1].y - yO];
dRel += `l${values
.map((val) => {
return +val.toFixed(decimals);
})
.join(" ")}`;
}
return dRel;
}
function renderPoint(svg, coords, fill = "red", r = "2") {
let marker =
'<circle cx="' +
coords[0] +
'" cy="' +
coords[1] +
'" r="' +
r +
'" fill="' +
fill +
'" ><title>' +
coords.join(", ") +
"</title></circle>";
svg.insertAdjacentHTML("beforeend", marker);
}
svg {
border: 1px solid #ccc;
width: 45vw;
overflow: visible;
margin-right: 1vw;
}
path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke-opacity: 0.5;
}
<svg id="svg" viewBox="0 0 40 30">
<path d="M 0 10 L 10 20 20 10 L 30 20 40 10" fill="none" stroke="#999" stroke-width="1"></path>
<path id="path" d="" fill="none" stroke="red" stroke-width="1" />
</svg>
<svg id="svg2" viewBox="0 0 40 30">
<path d="M 0 10 L 10 20 20 10 L 30 20 40 10" fill="none" stroke="#999" stroke-width="1"></path>
<path id="pathRel" d="" fill="none" stroke="red" stroke-width="1" />
</svg>
Example: Svg draw Pad
const svg = document.getElementById("svg");
const svgns = "http://www.w3.org/2000/svg";
let strokeWidth = 0.25;
// rounding and smoothing
let decimals = 2;
let getNthMouseCoord = 1;
let smooth = 2;
// init
let isDrawing = false;
var points = [];
let path = "";
let pointCount = 0;
const drawStart = (e) => {
pointCount = 0;
isDrawing = true;
// create new path
path = document.createElementNS(svgns, "path");
svg.appendChild(path);
};
const draw = (e) => {
if (isDrawing) {
pointCount++;
if (getNthMouseCoord && pointCount % getNthMouseCoord === 0) {
let point = getMouseOrTouchPos(e);
// save to point array
points.push(point);
}
if (points.length > 1) {
let d = smoothQuadratic(points, smooth, decimals);
path.setAttribute("d", d);
}
}
};
const drawEnd = (e) => {
isDrawing = false;
points = [];
// just illustrating the ouput
svgMarkup.value = svg.outerHTML;
};
// start drawing: create new path;
svg.addEventListener("mousedown", drawStart);
svg.addEventListener("touchstart", drawStart);
svg.addEventListener("mousemove", draw);
svg.addEventListener("touchmove", draw);
// stop drawing, reset point array for next line
svg.addEventListener("mouseup", drawEnd);
svg.addEventListener("touchend", drawEnd);
svg.addEventListener("touchcancel", drawEnd);
function smoothQuadratic(points, skip = 0, decimals = 3) {
let pointsL = points.length;
let even = pointsL - skip - (1 % 2) === 0;
// set M/starting point
let type = "M";
let values = [points[0].x, points[0].y];
let [Mx, My] = values.map((val) => {
return +val.toFixed(decimals);
});
let dRel = `${type}${Mx} ${My}`;
// offsets for relative commands
let xO = Mx;
let yO = My;
// split 1st line segment
let [x1, y1] = [points[1].x, points[1].y];
let [xM, yM] = [(Mx + x1) / 2, (My + y1) / 2];
let [xMR, yMR] = [xM - xO, yM - yO].map((val) => {
return +val.toFixed(decimals);
});
dRel += `l${xMR} ${yMR}`;
xO += xMR;
yO += yMR;
for (let i = 1; i < points.length; i += 1 + skip) {
// control point
let [x, y] = [points[i].x, points[i].y];
let [xR, yR] = [x - xO, y - yO];
// next point
let [xN, yN] = points[i + 1 + skip] ?
[points[i + 1 + skip].x, points[i + 1 + skip].y] :
[points[pointsL - 1].x, points[pointsL - 1].y];
let [xNR, yNR] = [xN - xO, yN - yO];
// mid point
let [xM, yM] = [(x + xN) / 2, (y + yN) / 2];
let [xMR, yMR] = [(xR + xNR) / 2, (yR + yNR) / 2];
type = "q";
values = [xR, yR, xMR, yMR];
// switch to t command
if (i > 1) {
type = "t";
values = [xMR, yMR];
}
dRel += `${type}${values
.map((val) => {
return +val.toFixed(decimals);
})
.join(" ")} `;
xO += xMR;
yO += yMR;
}
// add last line if odd number of segments
if (!even) {
values = [points[pointsL - 1].x - xO, points[pointsL - 1].y - yO];
dRel += `l${values
.map((val) => {
return +val.toFixed(decimals);
})
.join(" ")}`;
}
return dRel;
}
/**
* based on:
* #Daniel Lavedonio de Lima
* https://stackoverflow.com/a/61732450/3355076
*/
function getMouseOrTouchPos(e) {
let x, y;
// touch cooordinates
if (
e.type == "touchstart" ||
e.type == "touchmove" ||
e.type == "touchend" ||
e.type == "touchcancel"
) {
let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0];
x = touch.pageX;
y = touch.pageY;
} else if (
e.type == "mousedown" ||
e.type == "mouseup" ||
e.type == "mousemove" ||
e.type == "mouseover" ||
e.type == "mouseout" ||
e.type == "mouseenter" ||
e.type == "mouseleave"
) {
x = e.clientX;
y = e.clientY;
}
// get svg user space coordinates
let point = svg.createSVGPoint();
point.x = x;
point.y = y;
let ctm = svg.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
return point;
}
body {
margin: 0;
font-family: sans-serif;
padding: 1em;
}
* {
box-sizing: border-box;
}
svg {
width: 100%;
max-height: 75vh;
overflow: visible;
}
textarea {
width: 100%;
min-height: 50vh;
resize: none;
}
.border {
border: 1px solid #ccc;
}
path {
fill: none;
stroke: #000;
stroke-linecap: round;
stroke-linejoin: round;
}
input[type="number"] {
width: 3em;
}
input[type="number"]::-webkit-inner-spin-button {
opacity: 1;
}
#media (min-width: 720px) {
svg {
width: 75%;
}
textarea {
width: 25%;
}
.flex {
display: flex;
gap: 1em;
}
.flex * {
flex: 1 0 auto;
}
}
<h2>Draw quadratic bezier (relative commands)</h2>
<p><button type="button" id="clear" onclick="clearDrawing()">Clear</button>
<label>Get nth Mouse position</label><input type="number" id="nthMouseCoord" value="1" min="0" oninput="changeVal()">
<label>Smooth</label><input type="number" id="simplifyDrawing" min="0" value="2" oninput="changeVal()">
</p>
<div class="flex">
<svg class="border" id="svg" viewBox="0 0 200 100">
</svg>
<textarea class="border" id="svgMarkup"></textarea>
</div>
<script>
function changeVal() {
getNthMouseCoord = +nthMouseCoord.value + 1;
simplify = +simplifyDrawing.value;;
}
function clearDrawing() {
let paths = svg.querySelectorAll('path');
paths.forEach(path => {
path.remove();
})
}
</script>
How it works
save mouse/cursor positions in a point array via event listeners
Event Listeners (including touch events):
function getMouseOrTouchPos(e) {
let x, y;
// touch cooordinates
if (e.type == "touchstart" || e.type == "touchmove" || e.type == "touchend" || e.type == "touchcancel"
) {
let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0];
x = touch.pageX;
y = touch.pageY;
} else if ( e.type == "mousedown" || e.type == "mouseup" || e.type == "mousemove" || e.type == "mouseover" || e.type == "mouseout" || e.type == "mouseenter" || e.type == "mouseleave") {
x = e.clientX;
y = e.clientY;
}
// get svg user space coordinates
let point = svg.createSVGPoint();
point.x = x;
point.y = y;
let ctm = svg.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
return point;
}
It's crucial to translate HTML DOM cursor coordinates to SVG DOM user units unless your svg viewport corresponds to the HTML placement 1:1.
let ctm = svg.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
optional: skip cursor points and use every nth point respectively (pre processing – aimed at reducing the total amount of cursor coordinates)
optional: similar to the previous measure: smooth by skipping polyine segments – the curve control point calculation will skip succeeding mid and control points (post processing – calculate curves based on retrieved point array but skip points).
Q to T simplification: Since we are splitting the polyline coordinates evenly we can simplify the path d output by using the quadratic shorthand command T repeating the previous tangents.
Converting to relative commands and rounding
Based on x/y offsets globally incremented by the previous command's end point.
Depending on your layout sizes you need to tweak smoothing values.
For a "micro smoothing" you should also include these css properties:
path {
fill: none;
stroke: #000;
stroke-linecap: round;
stroke-linejoin: round;
}
Further reading
Change T command to Q command in SVG
There are already some implementations for this on github e.g. https://github.com/epistemex/cardinal-spline-js
You dont have to change anything on your input for that and can only change the draw function, that the line between the points is smooth. With that the points dont slip a bit during the simplification.

Categories