function pointerTrace(opts) {
const {
width: w,
height: h,
initTrace,
eventTypes,
render,
filter,
showButtons,
backgroundImage
} = {
width: width,
height: 280,
initTrace: [],
eventTypes: pointerEventTypes,
render: renderPoints,
filter: event => true,
showButtons: true,
backgroundImage: undefined,
...opts
};
const control = html`<div></div>`;
const statusText = html`<i></i>`;
statusText.style.marginRight = "1em";
const context = DOM.context2d(w, h);
const canvas = context.canvas;
canvas.setAttribute('touch-action', "none");
canvas.style.display = 'block';
const curve = d3.curveBasis(context);
const _trace = JSON.parse(JSON.stringify(initTrace));
function storePoint(x, y, eventType) {
_trace.push({ x, y, eventType, datetime: new Date().toISOString() });
}
function updateView() {
// update rendered view
render(context, _trace, { curve, height: h });
statusText.innerText =
_trace.length === 0
? `No point`
: _trace.length === 1
? `1 point`
: `${_trace.length} points`;
// update value (deep copy to be immutable)
// TODO: remove this? or provide a more efficient storage structure?
control.value = JSON.parse(JSON.stringify(_trace));
control.dispatchEvent(new CustomEvent("input"));
}
function handleEvent(event) {
const bbox = canvas.getBoundingClientRect();
if (filter(event)) {
storePoint(event.pageX - bbox.left, event.pageY - bbox.top, event.type);
}
updateView();
}
function addEventListeners() {
for (const eventType of eventTypes) {
canvas.addEventListener(eventType, handleEvent);
}
}
function removeEventListeners() {
for (const eventType of eventTypes) {
canvas.removeEventListener(eventType, handleEvent);
}
}
function start() {
addEventListeners();
invalidation.then(() => removeEventListeners());
}
function stop() {
removeEventListeners();
}
function reset() {
_trace.splice(0, _trace.length);
updateView();
}
function createCaptureButton() {
const captureButton = html`<button></button>`;
captureButton.style.marginRight = "1em";
captureButton.toStart = () => {
start();
captureButton.innerText = 'Stop';
captureButton.onclick = () => {
stop();
captureButton.toStop();
};
};
captureButton.toStop = () => {
stop();
captureButton.innerText = 'Start';
captureButton.onclick = () => {
start();
captureButton.toStart();
};
};
// initialize in "stop" mode
captureButton.toStop();
return captureButton;
}
function createClearButton() {
const clearButton = html`<button>Clear</button>`;
clearButton.style.marginRight = "1em";
clearButton.onclick = () => {
reset();
};
return clearButton;
}
function createSaveButton() {
const saveButton = DOM.download(
() =>
new Blob([JSON.stringify(_trace, null, 2)], {
type: "application/json"
}),
`pointer.json`,
`Save`
);
saveButton.style.marginRight = "1em";
return saveButton;
}
function setupButtons(showButtons) {
// by default: start. Latter init functions might change this behavior (createCaptureButton)
start();
const initFunctions = new Map([
['capture', createCaptureButton],
['clear', createClearButton],
['save', createSaveButton],
['info', () => statusText]
]);
const fullList = [...initFunctions.keys()];
const keys =
showButtons === true
? fullList
: Array.isArray(showButtons)
? showButtons.filter(s => fullList.includes(s))
: [];
canvas.style.marginBottom = '1em';
for (const key of keys) {
control.append(initFunctions.get(key)());
}
}
if (backgroundImage instanceof HTMLImageElement) {
const image = new Image();
image.src = backgroundImage.src;
image.style.objectPosition = 'left top';
image.style.objectFit = 'none';
image.width = w;
image.height = h;
image.style.position = "absolute";
canvas.style.position = "relative";
control.append(image);
}
control.append(canvas);
setupButtons(showButtons);
updateView();
return control;
}