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();
}