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/cafe-baladieh/js/script.js
var dataset = [];
var spanDocs = new Map();
var center = {x: 0, y: -5, r: 0};
var spread = {x: 80, y: 50, r: 60}
var hammering = false;
var fullscreen = false;

fa = num => {
  return `${num}`
    .replace(/0/g, '۰')
    .replace(/1/g, '۱')
    .replace(/2/g, '۲')
    .replace(/3/g, '۳')
    .replace(/4/g, '۴')
    .replace(/5/g, '۵')
    .replace(/6/g, '۶')
    .replace(/7/g, '۷')
    .replace(/8/g, '۸')
    .replace(/9/g, '۹');
}

$(async () =>{
  await initDocs(raw);
  initScrapboard();
  initTimeline();
  rearrange();

  $('.fullscreen').click(event => openFullscreen(document.documentElement));
  document.documentElement.addEventListener('fullscreenchange', event => {
    fullscreen = !!document.fullscreenElement;
    $('.fullscreen').toggleClass('hidden', document.fullscreenElement);
  });
});

function openFullscreen(elem) {
  if(fullscreen)
    document.exitFullscreen();
  else{
    if (elem.requestFullscreen) elem.requestFullscreen();
    else if (elem.mozRequestFullScreen) elem.mozRequestFullScreen();
    else if (elem.webkitRequestFullscreen) elem.webkitRequestFullscreen();
    else if (elem.msRequestFullscreen) elem.msRequestFullscreen();
  }
}

function initTimeline(){
  let spans = Array.from(spanDocs.entries()).map(entry => ({year: entry[1].year, span: entry[1].span})).reverse();
  let lastYear = 1357;
  let oneYearFlag = false;
  for(let span of spans) {
    let endYear = span.year+span.span;
    if(lastYear>endYear){
      let spanLength = lastYear-endYear;
      lastYear = endYear;
      
      if(spanLength>1) oneYearFlag = false;
      else oneYearFlag = !oneYearFlag;
      if(oneYearFlag) $('.time-span:first-child .date').remove();
      
      let spanEl = $(`
        <div class="time-span empty" data-year="${endYear}" style="--year-factor: ${spanLength};">
          <div class="line"></div>
          <div class="date">${fa(endYear)}</div>
        </div>`);
      $('.time-spans').prepend(spanEl);
    }
    let spanLength = lastYear-span.year;
    lastYear = span.year;
    
    if(spanLength>1) oneYearFlag = false;
    else oneYearFlag = !oneYearFlag;
    if(oneYearFlag) $('.time-span:first-child .date').remove();
    
    let spanEl = $(`
      <div class="time-span" data-year="${span.year}" style="--year-factor: ${spanLength};">
        <div class="line"></div>
        <div class="date">${fa(span.year)}</div>
      </div>`);
    $('.time-spans').prepend(spanEl);
  }
  $('.time-spans .time-span:last').addClass('last');

  $('.time-span').click(event => {
    if(hammering) return;
    $this = $(event.target).closest('.time-span');
    if($this.hasClass('empty')) return;

    let selectedYear = $this.data().year;
    $sts = $('.selected-time-span');
    if($this.hasClass('active')){
      selectedYear = 0;
      $this.toggleClass('active', false);
      $sts.toggleClass('visible', false);
      $('body').removeClass('in-lightbox');
    }else{
      $this
        .toggleClass('active', true)
        .siblings()
        .toggleClass('active', false);
      $sts
        .toggleClass('visible', true)
        .css('left', $this.position().left)
        .css('width', $this.width());
      $('body').addClass('in-lightbox');
    }

    rearrange(selectedYear);
  });

  let timeEl = $('.timeline')[0];
  let mc = new Hammer(timeEl);
  mc.on('panstart', event => {
    hammering = true;
    let timeEl = $('.timeline');
    timeEl.data('pose', timeEl.position().left);
    // timeEl.data('panning', true);
  });
  mc.on('pan', event => {
    event.srcEvent.preventDefault();
    let timeEl = $('.timeline');
    let left = timeEl.data('pose')+event.deltaX;
    let width = $('body').width();
    left = Math.min(width*.1, left);
    left = Math.max(-width*1.1, left);
    timeEl.css('left', `${left}px`);
  });
  mc.on('panend', event => {
    setTimeout(() => hammering = false, 300);
  });

  $('.timeline-section:first-child').click(event => {
    let width = $('body').width();
    // $('.timeline').css('left', `${width*.1}px`);
    $('.timeline').animate({left: width*.1}, 1000);
  });

  $('.timeline-section:last-child').click(event => {
    let width = $('body').width();
    // $('.timeline').css('left', `${-width*1.1}px`);
    $('.timeline').animate({left: -width*1.1}, 1000);
  });
}

function rearrange(year) {
  reorderZ();
  if(!year)
    $('.scrapboard .doc').each((i, doc) => {
      doc.style.setProperty('--x', `${(Math.random() - .5)*spread.x + center.x}vw`);
      doc.style.setProperty('--y', `${(Math.random() - .5)*spread.y + center.y}vh`);
      doc.style.setProperty('--r', `${(Math.random() - .5)*spread.r + center.r}deg`);
      doc.style.setProperty('--s', .5);
    });
  else
    updateLightbox(spanDocs.get(`_${year}`).docs);
}

function updateLightbox(lightbox){
  let $docs = $('.scrapboard .doc');
  let lightboxIds = lightbox.map(record => record.index);
  for(let doc of $docs) {
    if(lightboxIds.includes(parseInt(doc.id.substr(1)))) {
      doc.style.setProperty('--x', `${(Math.random() - .5)*spread.x*.7 + center.x}vw`);
      doc.style.setProperty('--y', `${(Math.random() - .5)*spread.y*.7 + center.y}vh`);
      doc.style.setProperty('--r', `${(Math.random() - .5)*spread.r + center.r}deg`);
      doc.style.setProperty('--s', .5);
      doc.classList.add('lightbox');
    }else{
      let docx = parseFloat(doc.style.getPropertyValue('--x').replace('vw', ''));
      if(docx == center.x) docx = center.x + Math.random()*10-5;
      let docy = parseFloat(doc.style.getPropertyValue('--y').replace('vh', ''));
      if(docy == center.y) docy = center.y + Math.random()*10-5;
      let distance = Math.sqrt((docx - center.x)**2 + (docy - center.y)**2);
      let maxDistance = Math.sqrt((spread.x/2)**2 + (spread.y/2)**2)*.9;
      let minDistance = maxDistance*.7;

      if(distance<minDistance || distance>maxDistance) {
        let factor = (minDistance + Math.random()*(maxDistance - minDistance))/distance;
        doc.style.setProperty('--x', `${factor*(docx - center.x) + center.x}vw`);
        doc.style.setProperty('--y', `${factor*(docy - center.y) + center.y}vh`);
        doc.style.setProperty('--r', `${(Math.random() - .5)*spread.r + center.r}deg`);
      }
      doc.style.setProperty('--s', .3);
      doc.classList.remove('lightbox');
    }
  }
}

function initDocs(raw) {
  let map = {};
  raw
    .split('\n')
    .map(line => line.split(','))
    .filter((record, i) => record.length==6)
    .forEach((record, i) => {
      if(!/^\d+$/.exec(record[0])) return;
      record[4] = record[4].trim();
      let key = `${record[2]}_${record[4]}`;
      if(!map[key]) {
        let yearFrom = parseInt(record[2].split('-')[0]);
        let yearTo   = parseInt(record[2].split('-')[1] || '0');
        if(!yearTo) yearTo = yearFrom;
        yearTo++;
        map[key] = {
          type: record[1],
          year: yearFrom,
          span: yearTo - yearFrom,
          docset: fa(record[3]),
          caption: record[4],
          files: []
        };

        let span = spanDocs.get(`_${map[key].year}`);
        if(!span){
          span = {year: map[key].year, span: map[key].span, docs: []};
          spanDocs.set(`_${map[key].year}`, span);
        }
        span.docs.push(map[key]);
      }
      map[key].files.push({
        file: record[0],
        cite: fa(record[5])
      });
    });

  dataset = Object
    .keys(map)
    .map(key => map[key]);

  return new Promise((resolve, reject) => {
    // return resolve(); /* ------------ */
    let setImage = i => {
      i--;
      if(i<0) return setTimeout(() => resolve(), 1000);

      let doc = dataset[i];
      doc.index = i;

      let $doc = $(`<div class="doc invisible" id="_${i}"></div>`);
      // for(let fileConf of doc.files)
        $doc.append(`<img class="thumb" src="docs-thumb/${doc.files[0].file}.jpg"></div>`);
      $('.scrapboard').append($doc);
      $doc.data('conf', doc);
      $doc.find('img:first-child')/*.addClass('visible')*/.on('load', event => {
        let $doc = $(event.target).closest('.doc');
        let w = event.target.naturalWidth; let h = event.target.naturalHeight;
        if(w>h) {
          h = 400*h/w;
          w = 400;
        }else{
          w = 400*w/h;
          h = 400;
        }
        $doc
          .css('width', `${w}px`)
          .css('height', `${h}px`)
          .css('margin-left', `${-w/2}px`)
          .css('margin-top', `${-h/2}px`)
          .toggleClass('invisible', false);
        $doc[0].style.setProperty('--ss', Math.min($('body').width()*.8/w, ($('body').height() - 230)*.8/h));
      });
      $doc.click(event => {
        if(hammering) return;
        if($doc.hasClass('spotlight'))
          hideSpotlight($doc);
        else
          showSpotlight($doc);
      });
      $('.spotlight-bg').click(event => {
        $('.doc.spotlight').click();
      });

      $('.lightbox-bg').click(event => {
        $('.time-span.active').click();
      });

      setTimeout(() => setImage(i), 0);
    };
    setImage(dataset.length);
  });
}

function hideSpotlight($doc) {
  $('body').toggleClass('in-spotlight', false);
  $doc
    .toggleClass('spotlight', false)
    .find('img.hires')
    .remove();
  $doc[0].style.setProperty('--z', ++maxz);
}

function showSpotlight($doc) {
  $('body').toggleClass('in-spotlight', true);
  for(let fileConf of $doc.data('conf').files)
    $doc.append(`<img class="hires" src="docs/${fileConf.file}.jpg"></div>`);
  $doc.find('img.hires:nth-child(2)').addClass('visible');
  $doc.toggleClass('spotlight', true);
  $('.spotlight-desc .doc-text').text($doc.data('conf').caption);
  $('.spotlight-desc .img-text').text($doc.data('conf').files[0].cite);
  let numBullets = $doc.data('conf').files.length;
  $('.spotlight-pagination .bullet').remove();
  $('.spotlight-pagination .bullet-index').remove();
  if(numBullets>1){
    $('.spotlight-pagination .bullets').removeClass('hidden').append($('<div class="bullet-index"></div>'));
    for(let i=0; i<numBullets; i++)
      $('.spotlight-pagination .bullets').append($('<div class="bullet"></div>').click(event => gotoPage(i)));
    alignBullet(0);
  }else
    $('.spotlight-pagination .bullets').addClass('hidden');
}

function nextPage() {
  let $doc = $('.doc.spotlight');
  let visible = $doc.find('img.visible').index() - 1;
  let next = (visible + 1) % $doc.data('conf').files.length;
  gotoPage(next);
}

function prevPage() {
  let $doc = $('.doc.spotlight');
  let visible = $doc.find('img.visible').index() - 1;
  let prev = visible ? visible - 1 : $doc.data('conf').files.length - 1;
  gotoPage(prev);
}

function gotoPage(i) {
  let $doc = $('.doc.spotlight');
  if($doc.data('conf').files.length<2) return;
  let $old = $doc.find('img.visible');
  let $new = $doc.find(`img:nth-child(${i+2})`);
  $('.spotlight-desc .img-text').text($doc.data('conf').files[i].cite);
  
  $old
    .removeClass('visible')
    .css('animation', `to-${$old.index()>i ? 'left' : 'right'} 1s`);
  $new
    .addClass('visible')
    .css('animation', `from-${$old.index()>i ? 'right' : 'left'} 1s`);

  alignBullet(i);
}

function alignBullet(i) {
  let pos = $(`.bullet:nth-child(${i+2})`).position();
  $('.bullets .bullet-index').css('left', pos.left+'px');
  $('.bullets .bullet-index').css('top', pos.top+'px');
}

maxz = 1;
function initScrapboard() {
  $('.scrapboard .doc').each((index, el) => {
    let $doc = $(el);
    var mc = new Hammer(el);
    var pinch = new Hammer.Pinch();
    var rotate = new Hammer.Rotate();
    pinch.recognizeWith(rotate);
    mc.add([pinch, rotate]);
    
    mc.get('pan').set({ direction: Hammer.DIRECTION_ALL });
    mc.on("panstart", event => {
      $doc.toggleClass('panning', true);
      let style = $doc[0].style;
      $doc[0].style.setProperty('--z', ++maxz);
      $doc.data('offset', {
        x: event.srcEvent.clientX - $('body').width()*(.5 + style.getPropertyValue('--x').replace('vw', '')/100),
        y: event.srcEvent.clientY - $('body').height()*(.5 + style.getPropertyValue('--y').replace('vh', '')/100)
      });
      if(maxz>1000) reorderZ();
      hammering = true;
    });
    mc.on("pan pinch", event => {
      if($doc.hasClass('spotlight')) return;
      $doc[0].style.setProperty('--x', (100*(event.srcEvent.clientX-$doc.data('offset').x)/$('body').width() - 50)+'vw');
      $doc[0].style.setProperty('--y', (100*(event.srcEvent.clientY-$doc.data('offset').y)/$('body').height() - 50)+'vh');
    });
    mc.on("panend pinchend", event => {
      $doc.toggleClass('panning', false);
      setTimeout(() => hammering = false, 300);
    });
    mc.on('swipeleft swiperight', event => {
      if(!$doc.hasClass('spotlight')) return;
      if($doc.find('img').length<2) return;
      if(event.type=='swiperight')
        nextPage($doc);
      else
        prevPage($doc);
    })
  });
}

function reorderZ() {
  maxz = 1;
  $('.scrapboard .doc')
    .map((i, el) => ({el: el, z: parseInt(el.style.getPropertyValue('--z')) || 1}))
    .sort((a, b) => a.z-b.z)
    .map((i, record) => record.el.style.setProperty('--z', maxz++));
}