Practice IV. Line effects

– RxJS version

2021-09-12

Inspired by JParticles, I decided to make something similar using RxJs for practice. The source code concatenated follows, but you can also visit the GitHub repo.

Source code

import { map, debounceTime } from 'rxjs/operators';
import { BehaviorSubject, fromEvent, Scheduler, interval } from 'rxjs';

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const colors = [
  '#3b4252',
  '#e1e1e1',
  '#c3c3c3',
  '#a5a5a5',
  '#878787',
  '#696969',
];
const darkGray = colors[0];
const background = '#fffff8';
const nNodes = (canvas.width * canvas.height) / 5000;
const initialState = initState(nNodes);
const nodes$ = new BehaviorSubject(initialState);

function next({ x, y }) {
  const { nodes } = nodes$.getValue();
  nodes$.next({
    nodes,
    mousePosClicked: {
      x,
      y,
    },
  });
}

const _updateNode = (n, _, ns) => updateNode(n, ns);

function update() {
  const { mousePosClicked, nodes } = nodes$.getValue();
  nodes$.next({
    mousePosClicked,
    nodes: nodes.map(_updateNode),
  });
}

function render() {
  clearCanvas();
  const { mousePosClicked, nodes } = nodes$.getValue();
  nodes.forEach(renderNode(mousePosClicked, nodes));
}

const clicks = fromEvent(canvas, 'click');
clicks.pipe(debounceTime(50), map(getMouseXY)).subscribe({ next });
interval(0, Scheduler.animationFrame).subscribe(render);
interval(Math.round(1000 / 60)).subscribe(update);

function getRandomValue(min, max) {
  return Math.floor(Math.random() * (max - min) + min);
}

function getMouseXY(e) {
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  const x = Math.round((e.clientX - rect.left) * scaleX);
  const y = Math.round((e.clientY - rect.top) * scaleY);
  return { x, y };
}

function checkPoint(a, b, x, y, r) {
  const distPoints = (a - x) * (a - x) + (b - y) * (b - y);
  return distPoints < r ** 2;
}

function getRandomValueNoZero(v1, v2) {
  const randomValue = getRandomValue(v1, v2 + 1);
  return randomValue !== 0 ? randomValue : getRandomValueNoZero(v1, v2);
}

function initState(nNodes) {
  return {
    nodes: Array.from({ length: nNodes }, (_) => {
      const val = getRandomValue(1, 5);
      const dx = getRandomValueNoZero(-5, 5);
      const dy = getRandomValueNoZero(-5, 5);
      return {
        x: getRandomValue(-100, window.innerWidth + 100),
        y: getRandomValue(-100, window.innerHeight + 100),
        dx,
        dy,
        color: colors[val],
        size: val,
        ns: [],
      };
    }),
    mousePosClicked: {
      x: -1000,
      y: -1000,
    },
  };
}

function clearCanvas() {
  context.fillStyle = background;
  context.fillRect(0, 0, window.innerWidth, window.innerHeight);
}

function drawLine(x1, y1, color, x2, y2, w) {
  context.strokeStyle = color;
  context.lineWidth = w;
  context.beginPath();
  context.moveTo(x1, y1);
  context.lineTo(x2, y2);
  context.stroke();
}

function drawCircle(x, y, color, radius) {
  context.fillStyle = color;
  context.beginPath();
  context.arc(x, y, radius, 0, 2 * Math.PI);
  context.fill();
}

function drawLinkNeighbors(node, ns) {
  for (let i = 0; i < ns.length; i++) {
    drawLine(node.x, node.y, node.color, ns[i].x, ns[i].y, node.size);
  }
}

function renderNode({ x, y }) {
  return (node) => {
    if (checkPoint(x, y, node.x, node.y, 200)) {
      drawLine(x, y, darkGray, node.x, node.y);
    }
    drawLinkNeighbors(node, node.ns);
    drawCircle(node.x, node.y, node.color, node.size);
  };
}

function updateNode(n, nodes) {
  const dx = n.x < 0 || n.x > window.innerWidth ? -n.dx : n.dx;
  const dy = n.y < 0 || n.y > window.innerHeight ? -n.dy : n.dy;
  const ns = nodes.filter((node) => checkPoint(n.x, n.y, node.x, node.y, 100));
  return {
    ...n,
    dx,
    dy,
    x: n.x + dx,
    y: n.y + dy,
    ns,
  };
}

About | Archive