<canvas id="c"></canvas>
<footer>
<h3>Made with <i class='fa fa-heart' ></i> by <a href="http://www.velasquezdaniel.com" target='_blank'>Daniel Velasquez</a>
</footer>
<!-- No longer used controls
<div id='controls'>
<div id='c-mouse' class='cc'>
<div class='header'>
<h3 class='header-title'>Mouse</h3>
<h3 class='header-value' id='mouse-value'>200</h3>
</div>
<input type="range" min="50" max="600" value="400" class="slider" id="mouse">
</div>
<div id='c-speed' class='cc'>
<div class='header'>
<h3 class='header-title'>Speed</h3>
<h3 class='header-value' id='speed-value'>53</h3>
</div>
<input type="range" min="1" max="30" value="30" class="slider" id="speed">
</div>
<div id='c-lines' class='cc'>
<div class='header'>
<h3 class='header-title'>Lines</h3>
<h3 class='header-value' id='lines-value'>53</h3>
</div>
<input type="range" min="2" max="20" value="6" class="slider" id="lines">
</div>
<div id='c-points' class='cc'>
<div class='header'>
<h3 class='header-title'>Points<i class="fa fa-question-circle info"><h4 class='info-puf'>Every line is formed by many points.<br />The more points you have, the more bents the line can have.</h4></i></h3>
<h3 class='header-value' id='points-value'>53</h3>
</div>
<input type="range" min="3" max="100" value="50" class="slider" id="points">
</div>
<div id='c-padding' class='cc'>
<div class='header'>
<h3 class='header-title'>padding</h3>
<h3 class='header-value' id='padding-value'>53</h3>
</div>
<input type="range" min="5" max="45" value="5" class="slider" id="padding">
</div>
<div class='debug cc'>
<div id='debug-fps' class='debug-item'>
<div class='header'>
<h3 class='header-title'>FPS</h3>
</div>
<input type="checkbox" name="fps" value="fps" id='debug-fps' class='check'>
</div>
<div id='c-debug-points' class='debug-item'>
<div class='header'>
<h3 class='header-title'>Points</h3>
</div>
<input type="checkbox" name="points" value="points" id='debug-points' class='check'>
</div>
</div>
</div> -->
* {
box-sizing: border-box;
}
html {
font-size: 12px;
}
body, html {
width: 100%;
height: 100%;
overflow: hidden;
padding: 0;
margin: 0;
position: relative;
}
footer {
position: absolute;
width: 100%;
bottom: 0px;
display: flex;
align-items: center;
justify-content: center;
a, i {
color: #ff5050;
text-decoration: none;
}
a:hover {
color: red;
}
h3 {
font-size: 1.25rem;
letter-spacing: 0.05em;
font-weight: 400;
color: white;
margin: 0;
padding: 0.75em 1.5em;
background: rgba(10,9,14,0.8);
border-radius: 2px;
}
}
.cc {
width: 100%;
padding: 0 20px;
}
#controls {
position: absolute;
right: 0;
top: 0;
background: rgba(10,9,14,0.8);
box-shadow: 0 2px 4px rgba(0,0,0,0.4);
min-width: 200px;
max-height: 100vh;
border-radius: 2px;
padding: 20px 0;
& > div {
margin-bottom: 20px;
}
& > div:last-of-type {
margin-bottom: 0px;
width: 100%;
}
}
.slider {
width: 100%;
height: 25px;
-webkit-appearance: none;
background: #d3d3d3;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
}
.slider:hover {
opacity: 1;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
background: #ff5050;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 25px;
height: 25px;
background: #4CAF50;
cursor: pointer;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
&-title, &-value {
position: relative;
display: inline-block;
float: left;
margin: 0;
color: white;
font-weight: 400;
letter-spacing: 0.1em;
text-transform: capitalize;
}
&-value {
font-weight: 200;
text-align: right;
}
}
.debug {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
&-item {
width: 50%;
text-align: center;
& .header {
margin-bottom: 10px;
&-title {
width: 100%;
}
}
}
}
.check {
width: 20px;
height: 20px;
&:hover {
cursor: pointer;
}
}
.info {
position: relative;
left: 5px;
&-puf {
opacity: 0;
font-family: sans-serif;
position: absolute;
line-height: 1.6em;
top: -30px;
right: calc(100% + 15px);
margin: 0;
white-space: nowrap;
background: #282828;
border-radius: 2px;
z-index: 50;
padding: 0.5em 2em;
text-transform: none;
transition: all 0.2s ease-in-out;
user-select: none;
pointer-events: none;
font-weight: 400;
}
&:hover {
cursor: pointer;
}
&:hover &-puf {
opacity: 1;
user-select: none;
pointer-events: none;
}
}
@media screen and (min-width: 800px){
html {
font-size: 12px;
}
}
@media screen and (min-width: 1200px){
}
View Compiled
let canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
var W = window.innerWidth;
var H = window.innerHeight;
canvas.width = W;
canvas.height = H;
// Helper Functions
// Get number in the middle
function mid(){
const args = Array.from(arguments);
if(args.length < 3) return args[0] || 0;
const sorted = args.slice().sort((a,b)=> a - b);
return sorted[Math.round((sorted.length - 1) / 2)];
}
// Phytagoshananigans theory
const PY = (x,y) => Math.sqrt(Math.pow(Math.abs(x),2) + Math.pow(Math.abs(y),2),2);
// Fps calculator
const fpsHelper = function(onSecond){
let lastSec = Date.now();
let frames = 0;
let fps = 0;
return {
onFrame: ()=>{
if(((Date.now() - lastSec) / 1000) > 1) {
lastSec = Date.now();
fps = frames;
frames = 0;
if(onSecond)
onSecond(fps);
} else {
frames += 1;
}
},
getFPS: () => {
return fps;
}
}
}
// Initialization
const mouse = {
x: W / 2,
y: H / 2
}
// pading is in percentage
const config = {
nPoints: 20,
nLines: 20,
radius: 100,
padding: 40,
showFPS: false,
showPoints: false,
maxSpeed: 30,
}
window.onload = function(){
var gui = new dat.GUI({ closed: true, name:"config" });
gui.add(config,"nPoints",3,50).step(1).onFinishChange(function(val){
debouncedInit();
});
gui.add(config,"nLines",3,50).step(1).onFinishChange(function(val){
console.log('finish',val,config)
debouncedInit();
});;
gui.add(config,"radius",50,300).step(1);
gui.add(config,"padding",5,45).step(1).onFinishChange(function(val){
console.log(val)
debouncedUpdateX();
});
gui.add(config,"showFPS");
gui.add(config,"showPoints");
gui.add(config,"maxSpeed", 5, 100);
}
let pointsPerLine = 20;
let linesInScreen = 20;
let lines = [];
let homesX = [];
let homesY = [];
let padding = 40;
let max = 30;
let radius = 200;
let fpsObj = fpsHelper();
let debug = {
fps: false,
dots: false,
};
let rAF = null;
// Actual Code
// Update Line's dots Position
function updateLine(line,homeY){
let point, desiredX, desiredY, desiredH, desiredForce, desiredAngle, hvx, hvy, mvx, mvy, x, y, homeX, vx, vy;
let radius = config.radius;
let maxSpeed = config.maxSpeed;
for(var j = line.length - 1; j >= 0; j--){
point = line[j];
x = point.x;
y = point.y;
hvx = 0, hvy = 0;
// Home forces
homeX = homesX[j];
if(x !== homeX || y !== homeY) {
desiredX = homeX - x;
desiredY = homeY - y;
desiredH = PY(desiredX,desiredY);
desiredForce = Math.max(desiredH * 0.2,1);
desiredAngle = Math.atan2(desiredY,desiredX);
hvx = desiredForce * Math.cos(desiredAngle);
hvy = desiredForce * Math.sin(desiredAngle);
}
// Mouse Forces
mvx = 0, mvy = 0;
desiredX = x - mouse.x;
desiredY = y - mouse.y;
if(!(desiredX > radius || desiredY > radius || desiredY < -radius || desiredX < -radius)) {
desiredAngle = Math.atan2(desiredY,desiredX);
desiredH = PY(desiredX,desiredY);
desiredForce = Math.max(0,Math.min(radius - desiredH,radius));
mvx = desiredForce * Math.cos(desiredAngle);
mvy = desiredForce * Math.sin(desiredAngle);
}
// Combine and limit
vx = Math.round(mid((mvx + hvx) * 0.9, maxSpeed, -maxSpeed));
vy = Math.round(mid((mvy + hvy) * 0.9, maxSpeed, -maxSpeed));
// Dont let point get too far from home
if(vx != 0) {
point.x += vx;
}
if(vy != 0){
point.y += vy;
}
line[j] = point;
}
return line;
}
function timer(){
ctx.clearRect(0,0,W,H);
if(config.showFPS){
fpsObj.onFrame();
ctx.fillStyle = '#282828';
ctx.textAlign="start";
ctx.textBaseline="top";
ctx.font="50px Helvetica";
ctx.fillText(fpsObj.getFPS(),50,50);
}
let line, xc,yc, cur, curX, curY, next, dot;
for(var i = lines.length - 1; i >= 0; i--){
// Update before rendering
line = updateLine(lines[i],homesY[i]);
lines[i] = line;
ctx.beginPath();
ctx.strokeStyle = '#d2d2d2';
ctx.moveTo(line[line.length - 1].x,line[line.length - 1].y);
for(var j = line.length - 2; j > 1; j--){
cur = line[j];
curX = cur.x;
curY = cur.y;
next = line[j - 1];
xc = (curX + next.x) / 2;
yc = (curY + next.y) / 2;
// ctx.bezierCurveTo(curX,curY,xc,yc,xc,yc);
// ctx.bezierCurveTo(xc,yc,xc,yc,next.x,next.y);
// ctx.bezierCurveTo(curX,curY,curX,curY,next.x,next.y);
// ctx.bezierCurveTo(curX,curY,curX,curY,xc,yc);
// ctx.bezierCurveTo(curX,curY,xc,yc,next.x,next.y);
// ctx.bezierCurveTo(xc,yc,xc,yc,curX,curY);
ctx.quadraticCurveTo(curX,curY,xc,yc);
if(i===0){
// console.log(cur.x, cur.y);
}
}
// ctx.bezierCurveTo(line[j].x,line[j].y, line[j - 1].x, line[j - 1].y, line[j - 1].x, line[j - 1].y);
ctx.quadraticCurveTo(line[j].x,line[j].y, line[j - 1].x, line[j - 1].y);
ctx.stroke();
if(config.showPoints) {
for(j = line.length - 1; j >= 0; j--){
dot = line[j];
ctx.beginPath();
ctx.fillStyle ='red';
ctx.arc(dot.x, dot.y, 1, 0, 2 * Math.PI);
ctx.fill();
}
}
}
rAF = requestAnimationFrame(timer)
}
function point(x,y){
return {
x: x,
y: y,
hy: y,
hx: x,
}
}
function updateX(){
let line, calcPad;
if(rAF) {
cancelAnimationFrame(rAF);
rAF = null;
}
calcPad = (W * config.padding) / 100;
homesX = [];
for(var i = config.nLines; i >= 0; i--){
x = calcPad + (((W - calcPad * 2) / config.nLines) * i);
homesX.push(x);
}
timer();
}
function init(){
// Cancel if neededs
if(rAF) {
cancelAnimationFrame(rAF);
rAF = null;
}
lines = [];
homesX = [];
homesY = [];
let line = [], y = 0, x = 0;
let calcPad = (W * config.padding) / 100;
for(var i = config.nLines; i >= 0; i--){
line = [];
// Include padding in calculatio
y = calcPad + (((H - calcPad * 2) / config.nLines) * i);
homesY.push(y);
for(var j = config.nPoints; j >= 0; j--){
let x = Math.round((W / config.nPoints ) * j);
line.push(point(x,y));
console.log(x,y)
if(i === 0) {
homesX.push(x);
}
}
if(i == 0){
// homesY.reverse();
}
lines.push(line);
}
timer();
}
// Input Handles
const debouncedInit = _.debounce(init, 200)
const debouncedUpdateX = _.debounce(updateX, 200)
// Events
window.addEventListener('mousemove',(e)=>{
mouse.x = e.clientX;
mouse.y = e.clientY;
});
window.addEventListener('resize', (e) => {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
init();
});
init();