Skip to content

Instantly share code, notes, and snippets.

@Papipo
Created November 13, 2015 12:52
Show Gist options
  • Save Papipo/c26cc14107f0936d8973 to your computer and use it in GitHub Desktop.
Save Papipo/c26cc14107f0936d8973 to your computer and use it in GitHub Desktop.
Mithril occlusion culling for variable height elements.
var test = require('tape'),
m = require("mithril"),
occluder = require("lib/occluder");
function template(child) {
return m(".child", {key: child, style: {height: child + "px"}}, child);
}
function view(children, o) {
o = o || occluder();
return function() {
return m("#parent", {config: o.scroller, style: {overflow: "auto", height: "50px"}},
o.view(children, template)
);
};
}
function render(children, occ) {
m.mount(document.body, null);
m.mount(document.body, {view: view(children, occ)});
m.redraw(true);
return document.getElementById("parent");
}
function childrenValues(parent) {
return [].slice.call(parent.querySelectorAll(".child")).map(function(c) {
return c.innerHTML;
});
}
test("renders all children if they don't fill the parent", function(t) {
t.deepEqual(childrenValues(render([25])), ["25"]);
t.end();
});
test("renders a single child if it fills the parent", function(t) {
t.deepEqual(childrenValues(render([100])), ["100"]);
t.end();
});
test("renders the last child if scrolled to it", function(t) {
var parent = render([30,50]);
parent.onscroll = function() {
t.deepEqual(childrenValues(parent), ["50"]);
t.end();
};
parent.scrollTop = 30;
});
test("renders clip children", function(t) {
var parent = render([30, 30]);
parent.onscroll = function() {
t.deepEqual(childrenValues(parent), ["30", "30"]);
t.end();
};
parent.scrollTop = 5;
});
test("does not render past nor further children", function(t) {
var parent = render([50, 150, 50]);
parent.onscroll = function() {
t.deepEqual(childrenValues(parent), ["150"]);
wrapper = parent.firstChild;
t.equal(wrapper.style.height, "250px");
t.equal(wrapper.style.top, "-30px");
t.end();
};
parent.scrollTop = 80;
});
test("children can be updated", function(t) {
var myoccluder = occluder();
var children = [50, 50];
var parent = render(children, myoccluder);
children.push(100);
myoccluder.restart();
parent.onscroll = function() {
t.deepEqual(childrenValues(parent), ["50", "100"]);
wrapper = parent.firstChild;
t.equal(wrapper.style.height, "250px");
t.equal(wrapper.style.top, "-20px");
t.end();
};
parent.scrollTop = 70;
});
var m = require("mithril");
function occluder() {
var childrenHeights;
var totalHeight;
var viewport, offset;
var ready = false;
function scroller(el, isInit) {
if (!isInit) {
el.addEventListener("scroll", scroll);
viewport = el.offsetHeight;
}
}
function setup(el, isInit) {
childrenHeights = [];
totalHeight = 0;
for (var height, i = 0; i < el.childNodes.length; i++) {
height = el.childNodes[i].getBoundingClientRect().height;
childrenHeights.push(height);
totalHeight += height;
}
ready = true;
m.redraw();
}
function scroll(e) {
offset = e.target.scrollTop;
m.redraw(true);
}
function restart() {
ready = false;
m.redraw(true);
}
function view(children, template) {
if (ready) {
return renderOcclusion(children, template);
} else {
return m("", {config: setup}, children.map(template));
}
}
function renderOcclusion(children, template) {
var past = 0;
var shown = [];
for (var i = 0; i < children.length; i++) {
var height = childrenHeights[i];
if (offset >= past + height) {
past += height;
} else if(past < offset + viewport) {
past += height;
shown.push(children[i]);
} else {
break;
}
}
return m('', {style: occlusionStyle()},
m("", {style: {position: "relative", top: offset + "px"}},
shown.map(template)
)
);
}
function occlusionStyle() {
return {
position: "relative",
height: totalHeight + "px",
top: "-" + topOffset() + "px"
};
}
function topOffset() {
var past = 0;
for (var i = 0; i < childrenHeights.length; i++) {
if (past + childrenHeights[i] > offset) {
return offset - past;
} else {
past += childrenHeights[i];
}
}
}
return {
view: view,
restart: restart,
scroller: scroller
};
}
module.exports = occluder;
@Papipo
Copy link
Author

Papipo commented Nov 13, 2015

Usage:

var component = {
  controller: function() {
    return {
      occlusion: occluder()
    };
  },
  view: function(scope) {
    return m("", {config: scope.occlusion.scroller, style: {overflow: "auto", height: "500px"}},
      scope.occlusion.view(children, template)
    );
  }
};

If the children change, you'll have to call occluder.restart().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment