homotopy-io / homotopy-webclient

https://homotopy.io
26 stars 5 forks source link

Occlusion effect for tangles in projected 2d diagrams #25

Closed jamievicary closed 5 years ago

jamievicary commented 5 years ago

We need to replicate in the new version the occlusion effect for tangles, which was implemented in the old ANC SVG renderer: 3dtangle It works by organizing the strands into groups of common depth. It then draws these groups in order, starting with the shallowest. After drawing each group, it is drawn again as a mask with twice the line width.

jamievicary commented 5 years ago

For reference, here is the code.

function SVG_draw_crossing(diagram, data, vertex, svg, defs) {

    let wires = diagram.getWireDepths(vertex.level, vertex.singular_height);

    // Sort the wire segments by depth
    let segments = [];
    let s = wires.source_depths;
    let t = wires.target_depths;
    for (let i = 0; i < s.length; i++) {
        segments.push({ source: true, index: i, depth: s[i], edge: vertex.source_edges[i] });
    }
    for (let i = 0; i < t.length; i++) {
        segments.push({ source: false, index: i, depth: t[i], edge: vertex.target_edges[i] });
    }
    segments.sort((a, b) => a.depth - b.depth );

    // Group them by common depth
    let groups = [];
    let group = [];
    let left = Number.MAX_VALUE;
    let right = -Number.MAX_VALUE;
    for (let i = 0; i < segments.length; i++) {
        let segment = segments[i];
        left = Math.min(left, segment.edge.x);
        right = Math.max(right, segment.edge.x);
        if (group.length == 0 || group[0].depth == segment.depth) {
            group.push(segment);
        } else {
            groups.push(group);
            group = [segment];
        }
    }
    groups.push(group);

    // Draw the groups nearest-first
    if (data.mask_index == undefined) data.mask_index = 0;
    var transparent_str = SVG_move_to({ x: left - 2.5, y: vertex.y - 0.5 }) + SVG_line_to({ x: left - 2.5, y: vertex.y + 0.5 })+ SVG_line_to({ x: right + 2.5, y: vertex.y + 0.5 })+ SVG_line_to({ x: right + 2.5, y: vertex.y - 0.5 })+ SVG_line_to({ x: left - 2.5, y: vertex.y - 0.5 });
    let mask_id = null;
    let mask_paths = [];

    for (let i = 0; i < groups.length; i++) {
        let group = groups[i];

        /* Decide the common tangent vector for elements of this group.
        If any element is vertical, use a vertical tangent.
        If all elements are on left or right, use a vertical tangent.
        Otherwise, find the mean incidence angle.
        */
        let theta = null;
        let theta_sum = 0;
        let unique_sign = 0;
        for (let i=0; i<group.length; i++) {
            let s = group[i];
            if (s.edge.x == vertex.x) {
                theta = 0;
                break;
            }
            let dx = s.edge.x - vertex.x;
            let sign = dx == 0 ? 0 : dx < 0 ? -1 : +1;
            if (unique_sign != null) {
                if (unique_sign == 0 && sign != 0) unique_sign = sign;
                if (unique_sign != 0 && sign != 0 && unique_sign != sign) unique_sign = null;                
            }
            theta_sum += (s.source ? -1 : +1) * Math.atan(dx / 0.5);
        }
        if (theta == null) {
            if (unique_sign != null) {
                theta = 0;
            } else {
                theta = theta_sum / group.length;
            }
        }
        //g.appendChild(mask[0]);

        let c2l = 0.5;
        let factor = 0.35;
        let c2x_target = vertex.x + c2l * Math.sin(theta);
        let c2x_source = vertex.x - c2l * Math.sin(theta);
        let c2y_target = vertex.y + c2l * factor * Math.cos(theta);
        let c2y_source = vertex.y - c2l * factor * Math.cos(theta);
        let c1l = 0.4;
        let mask_mult = 2.5;
        let mask_url = mask_id ? "url(#" + mask_id + ")" : null;

        for (let j = 0; j < group.length; j++) {
            let segment = group[j];

            // Draw this segment
            let edge = segment.edge;
            let start_y = segment.source ? edge.finish_height - 0.5 : edge.start_height + 0.5;
            var segment_str = SVG_move_to({ x: edge.x, y: start_y })
                + SVG_bezier_to({ c1x: edge.x, c1y: start_y + (segment.source ? c1l : -c1l),
                    //c2x: edge.x, c2y: vertex.y,
                    c2x: segment.source ? c2x_source : c2x_target, c2y: segment.source ? c2y_source : c2y_target,
                    x: vertex.x, y: vertex.y });

            // Draw the segment and patching circles
            svg.appendChild(SVG_create_path({ string: segment_str, element_index: null, element_index_2: null, stroke: edge.colour, stroke_width: line_width, element_type: 'homotopy_edge' , mask: mask_url}));
            svg.appendChild(SVG_create_circle({x: vertex.x, y: vertex.y, fill: edge.colour, radius: line_width / 2, class_name: 'dummy'}));

            // Add appropriate paths to the mask to obscure later segments for a 3d effect
            mask_paths.push($(SVG_create_path({ string: segment_str, stroke_width: line_width * mask_mult, stroke: 'black', element_type: 'mask_top' })));
            mask_paths.push($(SVG_create_circle({x: vertex.x, y: vertex.y, fill: edge.colour, radius: line_width * mask_mult / 2, class_name: 'dummy'})));
        }

        // On the last pass we don't need to make the mask
        if (i == groups.length - 1) break;

        mask_id = 'mask' + (data.mask_index++);
        let mask_obj = $('<mask>', { id: mask_id, maskUnits: 'userSpaceOnUse' });
        mask_obj.append(SVG_create_path({ string: transparent_str, fill: 'white', element_type: 'mask_transparent' }));
        for (let i=0; i<mask_paths.length; i++) {
            mask_obj.append(mask_paths[i].clone());
        }
        defs.append(mask_obj);
    }

    // Attech the transparent circle over the homotopy to accept the mouse hover
    vertex.fill = vertex.colour; //gProject.getColour(vertex.type, vertex.dimension);
    vertex.radius = line_width;
    vertex.element_type = 'vertex';
    vertex.element_index = vertex.index;
    vertex.fill_opacity = 0;
    svg.appendChild(SVG_create_circle(vertex));
}
jamievicary commented 5 years ago

A lot of thought went into aspects of this, e.g. the trig that computes the control points leading to nice smooth curves, and I'd like to preserve this as far as possible.

jamievicary commented 5 years ago

And for reference here's some example SVG that it generates:

<svg preserveAspectRatio="xMidYMid meet" width="1085" height="644" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" style="overflow: hidden; ">
    <defs>
        <mask id="mask0" maskUnits="userSpaceOnUse">
            <path d="M -2.5 0 L -2.5 1 L 4.5 1 L 4.5 0 L -2.5 0 " stroke-width="0.1" stroke="none" fill="white" stroke-opacity="1" fill-opacity="1" element_type="mask_transparent"></path>
            <path d="M 0 0 C 0 0.4, 0.5527864045000421 0.4217376207875073, 1 0.5 " stroke-width="0.25" stroke="black" fill="none" stroke-opacity="1" fill-opacity="1" element_type="mask_top"></path>
            <circle cx="1" cy="0.5" r="0.125" fill="#000000" stroke="none" class="dummy"></circle>
            <path d="M 2 1 C 2 0.6, 1.4472135954999579 0.5782623792124927, 1 0.5 " stroke-width="0.25" stroke="black" fill="none" stroke-opacity="1" fill-opacity="1" element_type="mask_top"></path>
            <circle cx="1" cy="0.5" r="0.125" fill="#000000" stroke="none" class="dummy"></circle>
        </mask>
        <mask id="mask1" maskUnits="userSpaceOnUse">
            <path d="M -2.5 0 L -2.5 1 L 4.5 1 L 4.5 0 L -2.5 0 " stroke-width="0.1" stroke="none" fill="white" stroke-opacity="1" fill-opacity="1" element_type="mask_transparent"></path>
            <path d="M 0 0 C 0 0.4, 0.5527864045000421 0.4217376207875073, 1 0.5 " stroke-width="0.25" stroke="black" fill="none" stroke-opacity="1" fill-opacity="1" element_type="mask_top"></path>
            <circle cx="1" cy="0.5" r="0.125" fill="#000000" stroke="none" class="dummy"></circle>
            <path d="M 2 1 C 2 0.6, 1.4472135954999579 0.5782623792124927, 1 0.5 " stroke-width="0.25" stroke="black" fill="none" stroke-opacity="1" fill-opacity="1" element_type="mask_top"></path>
            <circle cx="1" cy="0.5" r="0.125" fill="#000000" stroke="none" class="dummy"></circle>
            <path d="M 1 0 C 1 0.4, 1 0.325, 1 0.5 " stroke-width="0.25" stroke="black" fill="none" stroke-opacity="1" fill-opacity="1" element_type="mask_top"></path>
            <circle cx="1" cy="0.5" r="0.125" fill="#000000" stroke="none" class="dummy"></circle>
            <path d="M 1 1 C 1 0.6, 1 0.675, 1 0.5 " stroke-width="0.25" stroke="black" fill="none" stroke-opacity="1" fill-opacity="1" element_type="mask_top"></path>
            <circle cx="1" cy="0.5" r="0.125" fill="#000000" stroke="none" class="dummy"></circle>
        </mask>
    </defs>
    <g id="viewport-20181114130918902" class="svg-pan-zoom_viewport" transform="matrix(361.6666666666667,0,0,361.6666666666667,180.83333333333331,502.83333333333337)">
        <g transform="scale (1 -1)" class="diagram_group">
            <path d="M -0.5 0 L 2.5 0 L 2.5 1 L -0.5 1 " stroke-width="0.1" stroke="none" fill="#ffffff" stroke-opacity="1" fill-opacity="1" element_type="region"></path>
            <path d="M 2 0 C 2 0.09999999999999998, 2 0.5, 1 0.5 C 2 0.5, 2 0.9, 2 1 L 2 1 L 2.5 1 L 2.5 0 L 2 0 " stroke-width="0.01" stroke="none" fill="#ffffff" element_type="region"></path>
            <path d="M 0 0 C 0 0.09999999999999998, 0 0.5, 1 0.5 C 1 0.5, 1 0.09999999999999998, 1 0 L 1 0 L 0 0 " stroke-width="0.01" stroke="none" fill="#ffffff" element_type="region"></path>
            <path d="M 1 0 C 1 0.09999999999999998, 1 0.5, 1 0.5 C 2 0.5, 2 0.09999999999999998, 2 0 L 2 0 L 1 0 " stroke-width="0.01" stroke="none" fill="#ffffff" element_type="region"></path>
            <path d="M 1 0.5 C 0 0.5, 0 0.9, 0 1 L 0 1 L 1 1 C 1 0.9, 1 0.5, 1 0.5 " stroke-width="0.01" stroke="none" fill="#ffffff" element_type="region"></path>
            <path d="M 1 0.5 C 1 0.5, 1 0.9, 1 1 L 1 1 L 2 1 C 2 0.9, 2 0.5, 1 0.5 " stroke-width="0.01" stroke="none" fill="#ffffff" element_type="region"></path>
            <path d="M 0 1 L 0 1 " stroke="#000000" stroke-width="0.1" fill="none" element_type="edge" element_index="3"></path>
            <path d="M 1 1 L 1 1 " stroke="#000000" stroke-width="0.1" fill="none" element_type="edge" element_index="4"></path>
            <path d="M 2 1 L 2 1 " stroke="#000000" stroke-width="0.1" fill="none" element_type="edge" element_index="5"></path>
            <path d="M 0 0 C 0 0.4, 0.5527864045000421 0.4217376207875073, 1 0.5 " stroke-width="0.1" stroke="#000000" fill="none" stroke-opacity="1" fill-opacity="1" element_type="homotopy_edge"></path>
            <circle cx="1" cy="0.5" r="0.05" fill="#000000" stroke="none" class="dummy"></circle>
            <path d="M 2 1 C 2 0.6, 1.4472135954999579 0.5782623792124927, 1 0.5 " stroke-width="0.1" stroke="#000000" fill="none" stroke-opacity="1" fill-opacity="1" element_type="homotopy_edge"></path>
            <circle cx="1" cy="0.5" r="0.05" fill="#000000" stroke="none" class="dummy"></circle>
            <path d="M 1 0 C 1 0.4, 1 0.325, 1 0.5 " stroke-width="0.1" stroke="#000000" fill="none" stroke-opacity="1" fill-opacity="1" element_type="homotopy_edge" mask="url(#mask0)"></path>
            <circle cx="1" cy="0.5" r="0.05" fill="#000000" stroke="none" class="dummy"></circle>
            <path d="M 1 1 C 1 0.6, 1 0.675, 1 0.5 " stroke-width="0.1" stroke="#000000" fill="none" stroke-opacity="1" fill-opacity="1" element_type="homotopy_edge" mask="url(#mask0)"></path>
            <circle cx="1" cy="0.5" r="0.05" fill="#000000" stroke="none" class="dummy"></circle>
            <path d="M 2 0 C 2 0.4, 1.4472135954999579 0.4217376207875073, 1 0.5 " stroke-width="0.1" stroke="#000000" fill="none" stroke-opacity="1" fill-opacity="1" element_type="homotopy_edge" mask="url(#mask1)"></path>
            <circle cx="1" cy="0.5" r="0.05" fill="#000000" stroke="none" class="dummy"></circle>
            <path d="M 0 1 C 0 0.6, 0.5527864045000421 0.5782623792124927, 1 0.5 " stroke-width="0.1" stroke="#000000" fill="none" stroke-opacity="1" fill-opacity="1" element_type="homotopy_edge" mask="url(#mask1)"></path>
            <circle cx="1" cy="0.5" r="0.05" fill="#000000" stroke="none" class="dummy"></circle>
            <circle cx="1" cy="0.5" r="0.1" fill="#777777" stroke="none" fill-opacity="0" element_index="0" element_type="vertex"></circle>
        </g>
    </g>
</svg>