Published
Edited
Dec 5, 2020
1 star
Insert cell
Insert cell
strokes
Insert cell
_ = require('lodash')
Insert cell
function Color({ color, onClick }) {
return html`
<div
style=${{
width: '100px',
height: '100px',
margin: '5px',
backgroundColor: color.value,
border: '1px solid black'
}}
onClick=${onClick}
>
</div>
`;
}
Insert cell
import { html, svg } from "@observablehq/htl"
Insert cell
Insert cell
height = 650
Insert cell
strokes
Insert cell
canvas = DOM.canvas()
Insert cell
Insert cell
viewof strokes = {
let canvas =
(this && this.querySelector('canvas')) ?? DOM.canvas(width, height);
let context = canvas.getContext('2d');

function clear() {
context.clearRect(0, 0, width, height);
}

let me = html`<div style=${{ userSelect: 'none' }}>
<button onClick=${() => clear()}>Clear</button>
${canvas}
</div>`;
me.value = (this && this.value) ?? [];

function repaint_strokes() {
clear();
me.value = me.value.slice(0, -2);
console.log(me.value);
for (let drawing of me.value) {
for (let segment of drawing.segments) {
draw(segment);
}
}
triggerInput();
}

let triggerInput = _.debounce(() => {
me.dispatchEvent(new CustomEvent('input'));
}, 100);

function draw(segment) {
let {
color,
stroke: { pressure, from, to }
} = segment;
// pressure is 0 for touch, 0.5 for mouse, and between 0 and 1 for pen.
if (from) {
context.beginPath();
context.lineWidth = 2 + 2 * (10 * pressure ** 3);
context.strokeStyle = color;
context.moveTo(...from);
context.lineTo(...to);
context.lineCap = "round";
context.stroke();
}
}
d3.select(canvas)
.on("touchmove", e => e.preventDefault()) // prevent scrolling
.on("pointerenter", () => (canvas.dbltap = canvas.dbltap || trackDbltap()))
.on("pointerdown", e => {
if (canvas.dbltap(e)) {
repaint_strokes();
return;
}
let untrack = trackPointer(e, {
start: stroke => {
let segment = {
stroke: stroke,
color: brush_color
};
draw(segment);
me.value.push({
segments: [segment]
});
triggerInput();
// console.log("start", stroke);
},
move: stroke => {
let segment = {
stroke: stroke,
color: brush_color
};
draw(segment);
me.value[me.value.length - 1].segments.push(segment);
triggerInput();

// console.log("move", stroke);
},
out: p => console.log("out", p),
end: p => console.log("end", p)
});
invalidation.then(untrack);
});

return me;
}
Insert cell
// track dbltap/dblclick:
// returns true if this pointerdown event is close in space and time to the previous one
function trackDbltap(delay = 500) {
let tap;
return function(e) {
const t = d3.pointer(e);
if (tap && Math.hypot(tap[0] - t[0], tap[1] - t[1]) < 20) return true;
tap = t;
setTimeout(() => (tap = null), delay);
};
}
Insert cell
// set up listeners that will follow this gesture all along
// (even outside the target canvas)
function trackPointer(e, { start, move, out, end }, invalidation) {
let id = e.pointerId;
let target = e.target;
let previous_point = null;

target.setPointerCapture(id);
let untrack = () => {
d3.select(target).on(`.${id}`, null);
target.releasePointerCapture(id);
}

d3.select(target)
.on(`pointerup.${id} pointercancel.${id}`, e => {
if (e.pointerId !== id) return;
untrack()
end?.({
from: previous_point,
to: null,
});
})
.on(`pointermove.${id}`, e => {
if (e.pointerId !== id) return;
let point = d3.pointer(e, target)
move?.({
from: previous_point,
pressure: e.pressure,
to: point,
});
previous_point = point
})
.on(`pointerout.${id}`, e => {
if (e.pointerId !== id) return;
out?.({
from: null,
to: null,
});
previous_point = null;
});

start?.({
point: d3.pointer(e, target),
sourceEvent: e
});
return untrack
}
Insert cell
d3 = require("d3-selection@2", "d3-color@2")
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