Published
Edited
Aug 26, 2020
1 fork
Importers
3 stars
Insert cell
Insert cell
viewof trace = pointerTrace()
Insert cell
Insert cell
trace
Insert cell
Insert cell
viewof traceWithOptions = pointerTrace({
width: 500,
height: 400,
initTrace: helloTrace,
eventTypes: ['pointerdown'],
render: renderLine('white'),
filter: event => event.pressure > 0,
showButtons: ['info', 'save'],
backgroundImage: await FileAttachment(
"ryan-stone-U3cctUBucn0-unsplash.jpg"
).image()
})
Insert cell
traceWithOptions
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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
};

// Init
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);

// Data
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;
}
Insert cell
function renderLine(color = 'black') {
return function(context, trace, { curve, height }) {
context.clearRect(0, 0, width, height);
context.strokeStyle = color;
context.beginPath();
curve.lineStart();
for (const p of trace) curve.point(p.x, p.y);
curve.lineEnd();
context.stroke();
};
}
Insert cell
function renderPoints(context, trace, { height }) {
const radius = 1;
context.clearRect(0, 0, width, height);

for (const p of trace) {
if (p.eventType === 'pointerdown') {
context.beginPath();
context.arc(p.x, p.y, 5 * radius, 0, Math.PI * 2, true);
context.stroke();
} else {
context.beginPath();
context.arc(p.x, p.y, radius, 0, Math.PI * 2, true);
context.fill();
}
}
}
Insert cell
Insert cell
d3 = require("d3-shape@1")
Insert cell
// Polyfill the pointer events if required
require("pepjs")
Insert cell
import { stroke as helloStroke } from '@observablehq/introduction-to-views'
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more