src/shapes/matrix.js
import BaseShape from './base-shape';
import TimelineTimeContext from '../core/timeline-time-context';
import LayerTimeContext from '../core/layer-time-context';
import PNGEncoder from '../utils/png.js';
const xhtmlNS = 'http://www.w3.org/1999/xhtml';
export default class Matrix extends BaseShape {
getClassName() {
return 'matrix';
}
_getAccessorList() {
// return { y: 0 };
return {};
}
// TODO determine suitable implementations for _getAccessorList and _getDefaults
_getDefaults() {
return {
normalise: 'none',
mapper: (value => {
// The mapper accepts a value, which is guaranteed to be in
// the range [0,1], and returns r, g, b components which are
// also in the range [0,1]. This example mapper just returns a
// grey level.
let level = 1.0 - value;
return [ level, level, level ];
}),
gain: 1.0,
smoothing: false, // NB with smoothing we get visible joins at tile boundaries
maxDataUriLength: 32767 // old IE browser limitation, others are more helpful
};
}
render(renderingCtx) {
console.log("matrix render called");
if (this.$el) { return this.$el; }
this.$el = document.createElementNS(this.ns, 'g');
if (!this.params.smoothing) {
// for Chrome
this.$el.setAttributeNS(null, 'image-rendering', 'pixelated');
}
console.log("matrix render returning");
return this.$el;
}
_hybridNormalise(gain) {
return (col => {
let max = 0.0;
for (let i = 0; i < col.length; ++i) {
let value = Math.abs(col[i]);
if (value > max) {
max = value;
}
}
let scale = gain;
if (max > 0.0) {
scale = scale * (Math.log10(max + 1.0) / max);
}
let n = [];
for (let i = 0; i < col.length; ++i) {
let value = col[i];
n.push(value * scale);
}
return n;
});
}
_columnNormalise(gain) {
return (col => {
let max = 0.0;
for (let i = 0; i < col.length; ++i) {
let value = Math.abs(col[i]);
if (value > max) {
max = value;
}
}
let scale = gain;
if (max > 0.0) {
scale = scale * (1.0 / max);
}
let n = [];
for (let i = 0; i < col.length; ++i) {
let value = col[i];
n.push(value * scale);
}
return n;
});
}
_noNormalise(gain) {
return (col => {
let n = [];
for (let i = 0; i < col.length; ++i) {
let value = col[i];
n.push(value * gain);
}
return n;
});
}
encache(matrixEntity) {
const before = performance.now();
console.log("matrix cache called");
const height = matrixEntity.getColumnHeight();
const totalWidth = matrixEntity.getColumnCount();
// We use one byte per pixel, as our PNG is indexed but
// uncompressed, and base64 encoding increases that to 4/3
// characters per pixel. The header and data URI scheme stuff add
// a further 1526 chars after encoding, which we round up to 1530
// for paranoia.
const maxPixels = Math.floor((this.params.maxDataUriLength * 3) / 4 - 1530);
let tileWidth = Math.floor(maxPixels / height);
if (tileWidth < 1) {
console.log("WARNING: Matrix shape tile width of " + tileWidth +
" calculated for height " + height +
", using 1 instead: this may exceed maxDataUriLength of " +
this.params.maxDataUriLength);
tileWidth = 1;
}
console.log("totalWidth = " + totalWidth + ", tileWidth = " + tileWidth);
let resources = [];
let widths = [];
let normalise = null;
switch (this.params.normalise) {
case 'hybrid':
normalise = this._hybridNormalise(this.params.gain);
break;
case 'column':
normalise = this._columnNormalise(this.params.gain);
break;
default:
normalise = this._noNormalise(this.params.gain);
break;
}
const condition = (col => {
let n = [];
for (let i = 0; i < col.length; ++i) {
if (col[i] === Infinity || isNaN(col[i])) n.push(0.0);
else n.push(col[i]);
}
return n;
});
const usualWidth = tileWidth;
const usualEncoder = new PNGEncoder(usualWidth, height, 256);
for (let x0 = 0; x0 < totalWidth; x0 += tileWidth) {
let w = tileWidth;
if (totalWidth - x0 < tileWidth) {
w = totalWidth - x0;
}
let p = (w === tileWidth ?
usualEncoder :
new PNGEncoder(w, height, 256));
for (let i = 0; i < w; ++i) {
const x = x0 + i;
let col = matrixEntity.getColumn(x);
col = normalise(condition(col));
for (let y = 0; y < height; ++y) {
let value = col[y];
// The value must be in the range [0,1] to pass to the
// mapper. We also quantize the range, as the PNG encoder
// uses a 256-level palette.
if (value < 0) value = 0;
if (value > 1) value = 1;
value = Math.round(value * 255) / 255;
let [ r, g, b ] = this.params.mapper(value);
if (r < 0) r = 0;
if (r > 1) r = 1;
if (g < 0) g = 0;
if (g > 1) g = 1;
if (b < 0) b = 0;
if (b > 1) b = 1;
const colour = p.color(Math.round(r * 255),
Math.round(g * 255),
Math.round(b * 255),
255);
const index = p.index(i, y);
p.buffer[index] = colour;
}
}
const resource = 'data:image/png;base64,' + p.getBase64();
resources.push(resource);
widths.push(w);
console.log("image " + resources.length + ": length " + resource.length +
" (dimensions " + w + " x " + height + ")");
}
console.log("drawing complete");
const after = performance.now();
console.log("matrix cache time = " + Math.round(after - before));
return {
resources: resources,
tileWidths: widths,
totalWidth: totalWidth,
height: height,
startTime: matrixEntity.getStartTime(),
stepDuration: matrixEntity.getStepDuration(),
elements: [] // will be installed in first call to update
};
}
update(renderingContext, cache) {
const before = performance.now();
console.log("matrix update called");
if (!cache.totalWidth || !cache.height ||
!renderingContext.width || !renderingContext.height) {
console.log("nothing to update");
return;
}
if (cache.elements.length === 0) {
console.log("About to add " + cache.resources.length +
" image resources to SVG...");
for (let i = 0; i < cache.resources.length; ++i) {
const resource = cache.resources[i];
const elt = document.createElementNS(this.ns, 'image');
elt.setAttributeNS('http://www.w3.org/1999/xlink', 'href', resource);
elt.setAttributeNS(null, 'preserveAspectRatio', 'none');
if (!this.params.smoothing) {
// for Firefox
elt.setAttributeNS(null, 'image-rendering', 'optimizeSpeed');
}
elt.addEventListener('dragstart', e => { e.preventDefault(); }, false);
this.$el.appendChild(elt);
cache.elements.push(elt);
}
console.log("Done that");
}
console.log("Render width = " + renderingContext.width);
let startX = renderingContext.timeToPixel(cache.startTime);
const drawnWidth = renderingContext.width - startX;
let widthScaleFactor = drawnWidth / cache.totalWidth;
if (cache.stepDuration > 0) {
let totalDuration = cache.stepDuration * cache.totalWidth;
let endX = renderingContext.timeToPixel(cache.startTime + totalDuration);
widthScaleFactor = (endX - startX) / cache.totalWidth;
}
let widthAccumulated = 0;
for (let i = 0; i < cache.elements.length; ++i) {
const elt = cache.elements[i];
const tileWidth = cache.tileWidths[i];
const x = startX + widthAccumulated * widthScaleFactor;
const w = tileWidth * widthScaleFactor;
const visible = (x + w > 0 && x < renderingContext.maxX);
if (visible) {
elt.setAttributeNS(null, 'x', Math.floor(x));
elt.setAttributeNS(null, 'width', Math.ceil(x + w) - Math.floor(x));
elt.setAttributeNS(null, 'y', 0);
elt.setAttributeNS(null, 'height', renderingContext.height);
elt.setAttributeNS(null, 'visibility', 'visible');
} else {
elt.setAttributeNS(null, 'visibility', 'hidden');
}
widthAccumulated += tileWidth;
}
const after = performance.now();
console.log("matrix update time = " + Math.round(after - before));
}
}