HEX
Server: nginx/1.24.0
System: Linux nowruzgan 6.8.0-57-generic #59-Ubuntu SMP PREEMPT_DYNAMIC Sat Mar 15 17:40:59 UTC 2025 x86_64
User: babak (1000)
PHP: 8.3.6
Disabled: NONE
Upload Files
File: /var/www/nowruzgan.com/vis/period-cooking/assets/script.js
class Animate {
  constructor(value) {
    this.value = value;
    this.timestamp = 0;
    this.from = 0;
    this._to = 0;
    this.progress = 0;
    this.inAction = false;
    this.duration = 400;
  }

  set to(value) {
    this.from = this.value;
    this._to = value;
    this.progress = 0;
    this.timestamp = Date.now();
    this.inAction = true;
  }

  get to() {
    return this._to;
  }

  proceed() {
    let dt = Date.now() - this.timestamp;
    this.progress = Math.min(1, dt/this.duration);
    this.inAction = this.progress < 1;
    this.value = this.from + (this.to-this.from)*this.progress;
  }
}

document.addEventListener('readystatechange', event => {
  if(event.target.readyState == 'complete') setup();
});

let canvas, c;
let activeShadow, asc;
let inactiveShadow, isc;
const width = 2000;

let dishCats = [];
let ingrCats = [];
let edges = [];
let selectedItem = undefined;

let activeAnimationList = new Map();

let conf = {
  dish: {
    angleFrom: -80,
    angleTo: 80,
    radius: .35*width,
    activeStyle: '#00ADD5',
    inactiveStyle: '#BFEAF5',
    isolatedStyle: '#00ADD5',
    textAlign: 'left',
    activeFont: '14px Farhang',
    inactiveFont: '14px Farhang',
    isolatedFont: '22px Farhang',
  },
  ingr: {
    angleFrom: -100,
    angleTo: -260,
    radius: .45*width,
    activeStyle: '#AA8801',
    inactiveStyle: '#EAE1BF',
    isolatedStyle: '#AA8801',
    textAlign: 'right',
    activeFont: '14px Farhang',
    inactiveFont: '14px Farhang',
    isolatedFont: '22px Farhang',
  },
  edge: {
    activeStyle: '#234B1010',
    inactiveStyle: '#E0E0E0',
    isolatedStyle: '#234B10FF',
  }
}

let bgAnimate = {
  active: new Animate(1),
  inactive: new Animate(0),
};

first = array => array[0];
last = array => array[array.length - 1];

async function setup() {
  canvas = document.getElementsByTagName('canvas')[0];
  c = canvas.getContext('2d');
  c.translate(width/2, width/2);
  activeShadow = document.createElement('canvas');
  activeShadow.setAttribute('width', 2000);
  activeShadow.setAttribute('height', 2000);
  asc = activeShadow.getContext('2d');
  asc.translate(width/2, width/2);
  inactiveShadow = document.createElement('canvas');
  inactiveShadow.setAttribute('width', 2000);
  inactiveShadow.setAttribute('height', 2000);
  isc = inactiveShadow.getContext('2d');
  isc.translate(width/2, width/2);
  await load();
  initInteraction();
  preDraw(true, asc);
  preDraw(false, isc);
  draw();
};

async function load(){
  let body = await fetch('./dishes.csv').catch(error => null);
  if(body === null) return console.log('error');

  let data = await body.text();
  data = data.split('\n').map(line => line.split(',').map(cell => cell.trim()));

  let cols = data.shift();
  data = data.map(record => {
    let newRecord = {};
    for(let i=0; i<record.length; i++)
      newRecord[cols[i]] = record[i];
    return newRecord;
  });

  let dishes = [];
  let ingres = [];
  currentDishCat = {name: ''};
  currentIngrCat = {name: ''};
  for(let record of data) {
    if(record.dishCategory != currentDishCat.name){
      currentDishCat = {name: record.dishCategory, items: [], parent: dishCats};
      dishCats.push(currentDishCat);
    }

    let ingrCat = ingrCats.find(cat => cat.name == record.ingredientCategory);
    if(!ingrCat) {
      ingrCat = {name: record.ingredientCategory, items: [], parent: ingrCats};
      ingrCats.push(ingrCat);
    }

    let dish = dishes.find(_dish => _dish.name == record.dish);
    if(!dish) {
      dish = {id: `dish-${record.dish}`, name: record.dish, type: 'dish', edges: [], parent: currentDishCat, _: new Animate(1)};
      currentDishCat.items.push(dish);
    }
    dishes.push(dish);

    let ingr = ingres.find(_ingr => _ingr.name == record.ingredient);
    if(!ingr) {
      ingr = {id: `dish-${record.ingredient}`, name: record.ingredient, type: 'ingr', edges: [], parent: ingrCat, _: new Animate(1)};
      ingrCat.items.push(ingr);
    }
    ingres.push(ingr);

    let edge = {id: `${dish.id}::${ingr.id}`, dish, ingr, _: new Animate(1)};
    dish.edges.push(edge);
    ingr.edges.push(edge);
    edges.push(edge);
  }

  ingrCats = ingrCats
    .sort((a, b) =>
      a.name>b.name ? 1 : a.name<b.name ? -1 : 0)
    .map(cat => {
      cat.items = cat.items
        .sort((a, b) =>
          a.name>b.name ? 1 : a.name<b.name ? -1 : 0);
      return cat;
    });

  let sum = 0;
  for(let cat of dishCats)
    sum += cat.items.length;
  sum += dishCats.length-1;
  sum--;
  dishCats.da = (conf.dish.angleTo - conf.dish.angleFrom)*Math.PI/(2*180*sum);
  let i = 0;
  for(let cat of dishCats){
    for(let dish of cat.items){
      dish.percent = (i++)/sum;
      dish.angle = Math.PI*(conf.dish.angleFrom + (conf.dish.angleTo - conf.dish.angleFrom)*dish.percent)/180;
      dish.x = Math.cos(dish.angle) * conf.dish.radius;
      dish.y = Math.sin(dish.angle) * conf.dish.radius;
    }
    i++;
  }

  sum = 0;
  for(let cat of ingrCats)
    sum += cat.items.length;
  sum += ingrCats.length-1;
  sum--;
  ingrCats.da = (conf.ingr.angleTo - conf.ingr.angleFrom)*Math.PI/(2*180*sum);
  i = 0;
  for(let cat of ingrCats){
    for(let ingr of cat.items){
      ingr.percent = (i++)/sum;
      ingr.angle = Math.PI*(conf.ingr.angleFrom + (conf.ingr.angleTo - conf.ingr.angleFrom)*ingr.percent)/180;
      ingr.x = Math.cos(ingr.angle) * conf.ingr.radius;
      ingr.y = Math.sin(ingr.angle) * conf.ingr.radius;
    }
    i++;
  }
}

function initInteraction() {
  canvas.addEventListener('mousemove', event => {
    let rect = event.target.getBoundingClientRect();
    let x = ((event.clientX - rect.x)/rect.width - .5)*width;
    let y = ((event.clientY - rect.y)/rect.height - .5)*width;
    let a = Math.atan2(y, x);
    if(a>Math.PI/2) a -= 2*Math.PI;
    let d = Math.sqrt(x**2 + y**2);

    let item = undefined;
    if(a>-Math.PI/2 && a<Math.PI/2 && d>conf.dish.radius+10) {
      for(let cat of dishCats){
        item = cat.items.find(item => a < item.angle+Math.abs(dishCats.da) && a > item.angle-Math.abs(dishCats.da));
        if(item) break;
      }
      if(item && item.width < (d-10)-conf.dish.radius)
        item = undefined;
    }else if(d>conf.ingr.radius+10) {
      for(let cat of ingrCats){
        item = cat.items.find(item => a < item.angle+Math.abs(ingrCats.da) && a > item.angle-Math.abs(ingrCats.da));
        if(item) break;
      }
      if(item && item.width < d-10-conf.ingr.radius)
        item = undefined;
    }

    if(item != selectedItem) {
      if(!selectedItem) {
        bgAnimate.active.to = 0;
        bgAnimate.inactive.to = .5;
      }else {
        if(!activeAnimationList.get(selectedItem.id))
          activeAnimationList.set(selectedItem.id, selectedItem);
        selectedItem._.to = 0;
      }

      if(!item) {
        bgAnimate.active.to = 1;
        bgAnimate.inactive.to = 0;
      }

      selectedItem = item;
      if(selectedItem){
        if(!activeAnimationList.get(selectedItem.id))
          activeAnimationList.set(selectedItem.id, selectedItem);
        selectedItem._.to = 1;
      }

      draw();
    }
  });
}

function preDraw(active, c) {
  c.clearRect(-width/2, -width/2, width, width);
  c.textBaseline = 'middle';

  c.strokeStyle = active ? conf.edge.activeStyle : conf.edge.inactiveStyle;
  for(let edge of edges) {
    c.beginPath();
    c.moveTo(edge.ingr.x, edge.ingr.y);
    c.bezierCurveTo(edge.ingr.x*.1, edge.ingr.y*.1, edge.dish.x*.1, edge.dish.y*.1, edge.dish.x, edge.dish.y);
    c.stroke();
  }

  c.fillStyle = active ? conf.dish.activeStyle : conf.dish.inactiveStyle;
  c.textAlign = conf.dish.textAlign;
  c.font = "14px Farhang";
  for(let cat of dishCats) {
    for(let dish of cat.items) {
      c.save();
      c.translate(dish.x, dish.y);
      c.rotate(dish.angle);
      c.fillText(dish.name, 10, 0);
      dish.width = c.measureText(dish.name).width;
      c.restore();
    }

    c.beginPath();
    c.arc(0, 0, conf.dish.radius+5, first(cat.items).angle-dishCats.da, last(cat.items).angle+dishCats.da);
    c.arc(0, 0, conf.dish.radius-5, last(cat.items).angle+dishCats.da, first(cat.items).angle-dishCats.da, true);
    c.fill();
  }

  c.fillStyle = active ? conf.ingr.activeStyle : conf.ingr.inactiveStyle;
  c.textAlign = conf.ingr.textAlign;
  for(let cat of ingrCats) {
    for(let ingr of cat.items) {
      c.save();
      c.translate(ingr.x, ingr.y);
      c.rotate(Math.PI + ingr.angle);
      c.fillText(ingr.name, -10, 0);
      ingr.width = c.measureText(ingr.name).width;
      c.restore();
    }

    c.beginPath();
    c.arc(0, 0, conf.ingr.radius+5, first(cat.items).angle-ingrCats.da, last(cat.items).angle+ingrCats.da, true);
    c.arc(0, 0, conf.ingr.radius-5, last(cat.items).angle+ingrCats.da, first(cat.items).angle-ingrCats.da);
    c.fill();
  }
}

function draw() {
  let t0 = Date.now();
  c.clearRect(-width/2, -width/2, width, width);

  c.globalAlpha = bgAnimate.active.value;
  c.drawImage(activeShadow, -width/2, -width/2);
  c.globalAlpha = bgAnimate.inactive.value;
  c.drawImage(inactiveShadow, -width/2, -width/2);

  let items = activeAnimationList.values();
  if(!activeAnimationList.size && selectedItem) items = [selectedItem];

  for(let item of items) {
    item._.proceed();
    c.globalAlpha = item._.value;
    drawText(item, 'isolated');
    for(let edge of item.edges){
      if(item.type=='dish') drawText(edge.ingr, 'isolated');
      else drawText(edge.dish, 'isolated');

      c.strokeStyle = conf.edge.isolatedStyle;
      c.beginPath();
      c.moveTo(edge.ingr.x, edge.ingr.y);
      c.bezierCurveTo(edge.ingr.x*.1, edge.ingr.y*.1, edge.dish.x*.1, edge.dish.y*.1, edge.dish.x, edge.dish.y);
      c.stroke();
    }

    if(!item._.inAction)
      activeAnimationList.delete(item.id);
  }
  c.globalAlpha = 1;

  let animationFlag = false;
  if(bgAnimate.active.inAction) {
    bgAnimate.active.proceed();
    animationFlag = true;
  }
  if(bgAnimate.inactive.inAction) {
    bgAnimate.inactive.proceed();
    animationFlag = true;
  }

  if(animationFlag || activeAnimationList.size)
    requestAnimationFrame(draw);

  // console.log('real', Date.now() - t0);
}

function drawText(item, state) {
    c.fillStyle = conf[item.type][`${state}Style`];
    c.textAlign = conf[item.type].textAlign;
    c.font = conf[item.type][`${state}Font`];
    c.save();
    c.translate(item.x, item.y);
    c.rotate(item.type=='dish' ? item.angle : Math.PI+item.angle);
    c.fillText(item.name, item.type=='dish' ? 10 : -10, 0);
    c.restore();
}