drawIntermediateLayer = (curLayerIndex, leftX, rightX, rightStart,
  intermediateGap, d, i, intermediateNodeMouseOverHandler,
  intermediateNodeMouseLeaveHandler, intermediateNodeClicked) => {
  
  // curRightX = rightStart;

  // Add the intermediate layer
  let intermediateLayer = svg.append('g')
    .attr('class', 'intermediate-layer')
    .style('opacity', 0);
  
  // Recovert the animation counter
  isEndOfAnimation = false;
  
  // Tried to add a rectangle to block the intermediate because of webkit's
  // horrible support (decade old bug) for foreignObject. It doesnt work either.
  // https://bugs.webkit.org/show_bug.cgi?id=23113
  // (1). ForeignObject's inside position is wrong on webkit
  // (2). 'opacity' of ForeignObject doesn't work on webkit
  // (3). ForeignObject always show up at the front regardless the svg
  //      stacking order on webkit

  let intermediateX1 = leftX + nodeLength + intermediateGap;
  let intermediateX2 = intermediateX1 + nodeLength + intermediateGap * 1.5;

  let range = cnnLayerRanges[selectedScaleLevel][curLayerIndex];
  let colorScale = layerColorScales[d.type];
  let intermediateMinMax = [];
  
  // Copy the previsious layer to construct foreignObject placeholder
  // Also add edges from/to the intermediate layer in this loop
  let linkData = [];

  // Accumulate the intermediate sum
  // let itnermediateSumMatrix = init2DArray(d.output.length,
  //  d.output.length, 0);

  // Compute the min max of all kernel weights in the intermediate layer
  let kernelExtents = d.inputLinks.map(link => getExtent(link.weight));
  let kernelExtent = kernelExtents.reduce((acc, cur) => {
    return [Math.min(acc[0], cur[0]), Math.max(acc[1], cur[1])];
  })
  let kernelRange = 2 * (Math.round(
    Math.max(...kernelExtent.map(Math.abs)) * 1000) / 1000);
  let kernelColorGap = 0.2;

  // Compute stride for the kernel animation
  let stride = kernelRectLength * 3; 

  // Also add the overlay mask on the output node
  let outputY = nodeCoordinate[curLayerIndex][i].y;
  let curNode = svg.select(`#layer-${curLayerIndex}-node-${i}`);
  let outputOverlayGroup = curNode.append('g')
    .attr('class', 'overlay-group')
    .attr('transform', `translate(${rightX}, ${outputY})`);

  let strideTime = Math.floor(nodeLength / stride);
  
  for (let i = 0; i < strideTime; i++) {
    for (let j = 0; j < strideTime; j++) {
      outputOverlayGroup.append('rect')
        .attr('class', `mask-overlay mask-${i}-${j}`)
        .attr('width', stride)
        .attr('height', stride)
        .attr('x', i * stride)
        .attr('y', j * stride)
        .style('fill', 'var(--light-gray)')
        .style('stroke', 'var(--light-gray)')
        .style('opacity', 1);
    }
  }

  // Make sure the bounding box is on top of other things
  curNode.select('rect.bounding').raise();

  // Add sliding kernel for the output node
  let kernelGroup = intermediateLayer.append('g')
    .attr('class', `kernel kernel-output kernel-clone`)
    .attr('transform', `translate(${rightX}, ${outputY})`);

  kernelGroup.append('rect')
    .attr('x', 0)
    .attr('y', 0)
    .attr('width', kernelRectLength * 3)
    .attr('height', kernelRectLength * 3)
    .attr('fill', 'none')
    .attr('stroke', intermediateColor);
  
  kernelGroup.attr('data-tick', 0)
    .attr('data-origin-x', rightX)
    .attr('data-origin-y', outputY);

  let delay = 200;
  let tickTime1D = nodeLength / (kernelRectLength * 3);

  startOutputAnimation(kernelGroup, tickTime1D, stride, delay, curLayerIndex);

  // First intermediate layer
  nodeCoordinate[curLayerIndex - 1].forEach((n, ni) => {

    // Compute the intermediate value
    let inputMatrix = cnn[curLayerIndex - 1][ni].output;
    let kernelMatrix = cnn[curLayerIndex][i].inputLinks[ni].weight;
    let interMatrix = singleConv(inputMatrix, kernelMatrix);

    // Compute the intermediate layer min max
    intermediateMinMax.push(getExtent(interMatrix));

    // Update the intermediate sum
    // itnermediateSumMatrix = matrixAdd(itnermediateSumMatrix, interMatrix);

    // Layout the canvas and rect
    let newNode = createIntermediateNode(curLayerIndex, i, intermediateLayer,
      intermediateX1, n.y, ni, stride, intermediateNodeMouseOverHandler,
      intermediateNodeMouseLeaveHandler, intermediateNodeClicked, true);
    
    // Draw the image
    let image = newNode.select('image');
    drawIntermidiateImage(image, range, colorScale, d.output.length,
      interMatrix);      

    // Edge: input -> intermediate1
    linkData.push({
      source: getOutputKnot({x: leftX, y: n.y}),
      target: getInputKnot({x: intermediateX1, y: n.y}),
      name: `input-${ni}-inter1-${ni}`
    });

    // Edge: intermediate1 -> intermediate2-1
    linkData.push({
      source: getOutputKnot({x: intermediateX1, y: n.y}),
      target: getInputKnot({x: intermediateX2,
        y: nodeCoordinate[curLayerIndex][i].y}),
      name: `inter1-${ni}-inter2-1`
    });

    // Create a small kernel illustration
    // Here we minus 2 because of no padding
    // let tickTime1D = nodeLength / (kernelRectLength) - 2;
    let kernelRectX = leftX - kernelRectLength * 3 * 2;
    let kernelGroup = intermediateLayer.append('g')
      .attr('class', `kernel kernel-${ni}`)
      .attr('transform', `translate(${kernelRectX}, ${n.y})`);

    let weightText = 'Kernel weights: [';
    let f2 = d3.format('.2f');
    for (let r = 0; r < kernelMatrix.length; r++) {
      for (let c = 0; c < kernelMatrix[0].length; c++) {
        kernelGroup.append('rect')
          .attr('class', 'kernel')
          .attr('x', kernelRectLength * c)
          .attr('y', kernelRectLength * r)
          .attr('width', kernelRectLength)
          .attr('height', kernelRectLength)
          .attr('fill', gappedColorScale(layerColorScales.weight, kernelRange,
            kernelMatrix[r][c], kernelColorGap));

        let sep = '';
        if (c === 0 && r == 0) { sep = ''; }
        else if (c === 0) { sep = '; '; }
        else { sep = ', '; }
        weightText = weightText.concat(sep, `${f2(kernelMatrix[r][c])}`);
      }
    }
    weightText = weightText.concat(']');

    kernelGroup.append('rect')
      .attr('x', 0)
      .attr('y', 0)
      .attr('width', kernelRectLength * 3)
      .attr('height', kernelRectLength * 3)
      .attr('fill', 'none')
      .attr('stroke', intermediateColor);
    
    kernelGroup.style('pointer-events', 'all')
      .style('cursor', 'crosshair')
      .on('mouseover', () => {
        hoverInfoStore.set( {show: true, text: weightText} );
      })
      .on('mouseleave', () => {
        hoverInfoStore.set( {show: false, text: weightText} );
      })
      .on('click', () => {d3.event.stopPropagation()});

    // Sliding the kernel on the input channel and result channel at the same
    // time
    let kernelGroupInput = kernelGroup.clone(true)
      .style('pointer-events', 'none')
      .style('cursor', 'pointer')
      .classed('kernel-clone', true)
      .classed(`kernel-input-${ni}`, true);

    kernelGroupInput.style('opacity', 0.9)
      .selectAll('rect.kernel')
      .style('opacity', 0.7);

    kernelGroupInput.attr('transform', `translate(${leftX}, ${n.y})`)
      .attr('data-tick', 0)
      .attr('data-origin-x', leftX)
      .attr('data-origin-y', n.y);

    let kernelGroupResult = kernelGroup.clone(true)
      .style('pointer-events', 'none')
      .style('cursor', 'pointer')
      .classed('kernel-clone', true)
      .classed(`kernel-result-${ni}`, true);

    kernelGroupResult.style('opacity', 0.9)
      .selectAll('rect.kernel')
      .style('fill', 'none');

    kernelGroupResult.attr('transform',
      `translate(${intermediateX1}, ${n.y})`)
      .attr('data-origin-x', intermediateX1)
      .attr('data-origin-y', n.y);
    
    startIntermediateAnimation(kernelGroupInput, kernelGroupResult, tickTime1D,
      stride);
  });

  // Aggregate the intermediate min max
  let aggregatedExtent = intermediateMinMax.reduce((acc, cur) => {
    return [Math.min(acc[0], cur[0]), Math.max(acc[1], cur[1])];
  })
  let aggregatedMinMax = {min: aggregatedExtent[0], max: aggregatedExtent[1]};

  // Draw the plus operation symbol
  let symbolY = nodeCoordinate[curLayerIndex][i].y + nodeLength / 2;
  let symbolRectHeight = 1;
  let symbolGroup = intermediateLayer.append('g')
    .attr('class', 'plus-symbol')
    .attr('transform', `translate(${intermediateX2 + plusSymbolRadius}, ${symbolY})`);
  
  symbolGroup.append('rect')
    .attr('x', -plusSymbolRadius)
    .attr('y', -plusSymbolRadius)
    .attr('width', 2 * plusSymbolRadius)
    .attr('height', 2 * plusSymbolRadius)
    .attr('rx', 3)
    .attr('ry', 3)
    .style('fill', 'none')
    .style('stroke', intermediateColor);
  
  symbolGroup.append('rect')
    .attr('x', -(plusSymbolRadius - 3))
    .attr('y', -symbolRectHeight / 2)
    .attr('width', 2 * (plusSymbolRadius - 3))
    .attr('height', symbolRectHeight)
    .style('fill', intermediateColor);

  symbolGroup.append('rect')
    .attr('x', -symbolRectHeight / 2)
    .attr('y', -(plusSymbolRadius - 3))
    .attr('width', symbolRectHeight)
    .attr('height', 2 * (plusSymbolRadius - 3))
    .style('fill', intermediateColor);

  // Place the bias rectangle below the plus sign if user clicks the firrst
  // conv node
  if (i == 0) {
    // Add bias symbol to the plus symbol
    symbolGroup.append('circle')
        .attr('cx', 0)
        .attr('cy', nodeLength / 2 + kernelRectLength)
        .attr('r', 4)
        .style('stroke', intermediateColor)
        .style('cursor', 'crosshair')
        .style('fill', gappedColorScale(layerColorScales.weight, kernelRange,
          d.bias, kernelColorGap))
        .on('mouseover', () => {
          hoverInfoStore.set( {show: true, text: `Bias: ${formater(d.bias)}`} );
        })
        .on('mouseleave', () => {
          hoverInfoStore.set( {show: false, text: `Bias: ${formater(d.bias)}`} );
        });

    // Link from bias to the plus symbol
    linkData.push({
      source: {x: intermediateX2 + plusSymbolRadius,
        y: nodeCoordinate[curLayerIndex][i].y + nodeLength},
      target: {x: intermediateX2 + plusSymbolRadius,
        y: nodeCoordinate[curLayerIndex][i].y + nodeLength / 2 + plusSymbolRadius},
      name: `bias-plus`
    });
  } else {
    // Add bias symbol to the plus symbol
    symbolGroup.append('circle')
      .attr('cx', 0)
      .attr('cy', -nodeLength / 2 - kernelRectLength)
      .attr('r', 4)
      .style('stroke', intermediateColor)
      .style('cursor', 'crosshair')
      .style('fill', gappedColorScale(layerColorScales.weight, kernelRange,
        d.bias, kernelColorGap))
      .on('mouseover', () => {
        hoverInfoStore.set( {show: true, text: `Bias: ${formater(d.bias)}`} );
      })
      .on('mouseleave', () => {
        hoverInfoStore.set( {show: false, text: `Bias: ${formater(d.bias)}`} );
      });
    
    // Link from bias to the plus symbol
    linkData.push({
      source: {x: intermediateX2 + plusSymbolRadius,
        y: nodeCoordinate[curLayerIndex][i].y},
      target: {x: intermediateX2 + plusSymbolRadius,
        y: nodeCoordinate[curLayerIndex][i].y + nodeLength / 2 - plusSymbolRadius},
      name: `bias-plus`
    });
  }

  // Link from the plus symbol to the output
  linkData.push({
    source: getOutputKnot({x: intermediateX2 + 2 * plusSymbolRadius - nodeLength,
      y: nodeCoordinate[curLayerIndex][i].y}),
    target: getInputKnot({x: rightX,
      y: nodeCoordinate[curLayerIndex][i].y}),
    name: `symbol-output`
  });
  
  // Output -> next layer
  linkData.push({
    source: getOutputKnot({x: rightX,
      y: nodeCoordinate[curLayerIndex][i].y}),
    target: getInputKnot({x: rightStart,
      y: nodeCoordinate[curLayerIndex][i].y}),
    name: `output-next`
  });

  // Draw the layer label
  intermediateLayer.append('g')
    .attr('class', 'layer-intermediate-label layer-label')
    .attr('transform', () => {
      let x = intermediateX1 + nodeLength / 2;
      let y = (svgPaddings.top + vSpaceAroundGap) / 2 + 5;
      return `translate(${x}, ${y})`;
    })
    .classed('hidden', detailedMode)
    .append('text')
    .style('text-anchor', 'middle')
    .style('dominant-baseline', 'middle')
    .style('font-weight', 800)
    .style('opacity', '0.8')
    .text('intermediate');
  
  intermediateLayer.append('g')
    .attr('class', 'animation-control')
    .attr('transform', () => {
      let x = intermediateX1 + nodeLength / 2;
      let y = (svgPaddings.top + vSpaceAroundGap) / 2 - 4;
      return `translate(${x}, ${y})`;
    })
    .on('click', () => animationButtonClicked(curLayerIndex))
    .append('image')
    .attr('class', 'animation-control-button')
    .attr('xlink:href', 'PUBLIC_URL/assets/img/fast_forward.svg')
    .attr('x', 50)
    .attr('y', 0)
    .attr('height', 13)
    .attr('width', 13);

  // Draw the detailed model layer label
  intermediateLayer.append('g')
    .attr('class', 'layer-intermediate-label layer-detailed-label')
    .attr('transform', () => {
      let x = intermediateX1 + nodeLength / 2;
      let y = (svgPaddings.top + vSpaceAroundGap) / 2 - 5;
      return `translate(${x}, ${y})`;
    })
    .classed('hidden', !detailedMode)
    .append('text')
    .style('text-anchor', 'middle')
    .style('dominant-baseline', 'middle')
    .style('opacity', '0.7')
    .style('font-weight', 800)
    .append('tspan')
    .text('intermediate')
    .append('tspan')
    .style('font-size', '8px')
    .style('font-weight', 'normal')
    .attr('x', 0)
    .attr('dy', '1.5em')
    .text(`(${cnn[curLayerIndex][0].output.length},
      ${cnn[curLayerIndex][0].output[0].length},
      ${cnn[curLayerIndex].length})`);

  // Draw the edges
  let linkGen = d3.linkHorizontal()
    .x(d => d.x)
    .y(d => d.y);
  
  let edgeGroup = intermediateLayer.append('g')
    .attr('class', 'edge-group')
    .lower();
  
  let dashoffset = 0;

  edgeGroup.selectAll('path')
    .data(linkData)
    .enter()
    .append('path')
    .classed('flow-edge', d => d.name !== 'output-next')
    .attr('id', d => `edge-${d.name}`)
    .attr('d', d => linkGen({source: d.source, target: d.target}))
    .style('fill', 'none')
    .style('stroke-width', 1)
    .style('stroke', intermediateColor);

  edgeGroup.select('#edge-output-next')
    .style('opacity', 0.1);
  
  edgeGroup.selectAll('path.flow-edge')
    .attr('stroke-dasharray', '4 2')
    .attr('stroke-dashoffset', 0)
    .each((d, i, g) => animateEdge(d, i, g, dashoffset - 1000));
  
  return {intermediateLayer: intermediateLayer,
    intermediateMinMax: aggregatedMinMax,
    kernelRange: kernelRange,
    kernelMinMax: {min: kernelExtent[0], max: kernelExtent[1]}};
}
Draw one intermediate layer
@param {number} curLayerIndex 
@param {number} leftX X value of intermediate layer left border
@param {number} rightX X value of intermediate layer right border
@param {number} rightStart X value of right component starting anchor
@param {number} intermediateGap The inner gap
@param {number} d Clicked node bounded data
@param {number} i Clicked node index
@param {function} intermediateNodeMouseOverHandler Mouse over handler
@param {function} intermediateNodeMouseLeaveHandler Mouse leave handler
@param {function} intermediateNodeClicked Mouse click handler