ink
This commit is contained in:
parent
dc12340294
commit
04ee89ba3b
|
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Ink</title>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
spatial-ink {
|
||||
position: absolute;
|
||||
inset: 0 0 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- <spatial-geometry x="100" y="100" width="50" height="50">Shape with some text</spatial-geometry> -->
|
||||
<spatial-ink
|
||||
size="16"
|
||||
points="[[100.2109375,380.546875,0.5],[100.2109375,380.5859375,0.5],[101.84375,376.14453125,0.5],[111.94921875,356.921875,0.5],[131.48828125,320.9609375,0.5],[159.8359375,267.92578125,0.5],[188.47265625,213.6015625,0.5],[212.35546875,167.1953125,0.5],[232.53515625,129.01171875,0.5],[244.046875,108.0390625,0.5],[250.87109375,96.34375,0.5],[254.7109375,90.04296875,0.5],[255.92578125,88.19140625,0.5],[256.41015625,87.796875,0.5],[256.6875,87.81640625,0.5],[256.8203125,88.10546875,0.5],[256.953125,88.73046875,0.5],[257.18359375,89.95703125,0.5],[257.71484375,92.921875,0.5],[258.69921875,99.1484375,0.5],[260.2109375,112.65625,0.5],[263.10546875,137.46875,0.5],[268.328125,184.9140625,0.5],[275.609375,251.04296875,0.5],[283.671875,323.125,0.5],[289.69140625,378.04296875,0.5],[293.0859375,409.7734375,0.5],[295.54296875,433.94921875,0.5],[297.07421875,446.9453125,0.5],[297.93359375,451.36328125,0.5],[298.31640625,452.4140625,0.5],[298.44921875,452.62890625,0.5],[298.5625,452.4765625,0.5],[298.58203125,452.0625,0.5],[298.58203125,451.61328125,0.5],[298.58203125,451.2890625,0.5],[298.58203125,451.0234375,0.5],[298.58203125,450.6328125,0.5],[298.58203125,450,0.5],[298.26953125,448.9140625,0.5],[297.55859375,446.9921875,0.5],[295.71484375,442.24609375,0.5],[290.54296875,430.1328125,0.5],[275.953125,400.171875,0.5],[249.8125,349.82421875,0.5],[218.24609375,292.22265625,0.5],[184.75,235.58984375,0.5],[152.42578125,182.49609375,0.5],[127.26953125,142.05859375,0.5],[106.046875,109.31640625,0.5],[86.51953125,78.953125,0.5],[72.625,57.46875,0.5],[64.6484375,44.73828125,0.5],[60.76171875,38.24609375,0.5],[58.890625,35.2890625,0.5],[58.41796875,34.43359375,0.5],[58.46875,34.3515625,0.5],[58.7109375,34.59375,0.5],[59.60546875,35.08203125,0.5],[63.8125,37.1328125,0.5],[76.5234375,43.61328125,0.5],[108.08203125,58.6875,0.5],[161.484375,83.85546875,0.5],[221.125,112.5234375,0.5],[276.8828125,139.140625,0.5],[318.1796875,158.30859375,0.5],[339.9765625,168.19921875,0.5],[352.5703125,173.86328125,0.5],[359,176.5703125,0.5],[361.00390625,177.43359375,0.5],[361.53515625,177.81640625,0.5],[361.6328125,178.00390625,0.5],[361.6328125,178.23046875,0.5],[361.515625,178.5078125,0.5],[360.953125,178.921875,0.5],[359.38671875,179.81640625,0.5],[356.04296875,182.3515625,0.5],[349.20703125,188.57421875,0.5],[335.9375,200.86328125,0.5],[314.625,220.796875,0.5],[283.6171875,248.703125,0.5],[244.09375,280.8984375,0.5],[202.40625,310.75390625,0.5],[167.30078125,332.05859375,0.5],[139.609375,345.65625,0.5],[120.84765625,353.5078125,0.5],[111.0625,356.7421875,0.5],[105.20703125,358.45703125,0.5],[102.74609375,358.9765625,0.5]]"
|
||||
></spatial-ink>
|
||||
<script type="module">
|
||||
import { SpatialGeometry } from '../src/canvas/spatial-geometry.ts';
|
||||
import { SpatialInk } from '../src/canvas/spatial-ink.ts';
|
||||
|
||||
SpatialGeometry.register();
|
||||
SpatialInk.register();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -9,7 +9,8 @@
|
|||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4"
|
||||
"leaflet": "^1.9.4",
|
||||
"perfect-freehand": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/leaflet": "^1.9.12",
|
||||
|
|
@ -699,16 +700,21 @@
|
|||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/perfect-freehand": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.2.tgz",
|
||||
"integrity": "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ=="
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.41",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
|
||||
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -726,8 +732,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
|
|
@ -769,9 +775,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -797,14 +803,14 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.1.tgz",
|
||||
"integrity": "sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==",
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.41",
|
||||
"rollup": "^4.13.0"
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"vite": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4"
|
||||
"leaflet": "^1.9.4",
|
||||
"perfect-freehand": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
import getStroke, { StrokeOptions } from 'perfect-freehand';
|
||||
|
||||
export type Point = [x: number, y: number, pressure: number];
|
||||
|
||||
export type Stroke = number[][];
|
||||
|
||||
const styles = new CSSStyleSheet();
|
||||
styles.replaceSync(`
|
||||
svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
:host(:state(tracing)) svg {
|
||||
position: fixed;
|
||||
inset: 0 0 0 0;
|
||||
z-index: calc(infinity);
|
||||
}
|
||||
`);
|
||||
|
||||
export class SpatialInk extends HTMLElement {
|
||||
static tagName = 'spatial-ink';
|
||||
|
||||
static register() {
|
||||
customElements.define(this.tagName, this);
|
||||
}
|
||||
|
||||
#internals = this.attachInternals();
|
||||
|
||||
#path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
|
||||
#stroke: Stroke = [];
|
||||
|
||||
#d = '';
|
||||
|
||||
#size = Number(this.getAttribute('size') || 32);
|
||||
|
||||
get size() {
|
||||
return this.#size;
|
||||
}
|
||||
|
||||
set size(size) {
|
||||
this.#size = size;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
#thinning = Number(this.getAttribute('thinning') || 0.5);
|
||||
|
||||
get thinning() {
|
||||
return this.#thinning;
|
||||
}
|
||||
|
||||
set thinning(thinning) {
|
||||
this.#thinning = thinning;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
#smoothing = Number(this.getAttribute('smoothing') || 0.5);
|
||||
|
||||
get smoothing() {
|
||||
return this.#smoothing;
|
||||
}
|
||||
|
||||
set smoothing(smoothing) {
|
||||
this.#smoothing = smoothing;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
#streamline = Number(this.getAttribute('streamline') || 0.5);
|
||||
|
||||
get streamline() {
|
||||
return this.#streamline;
|
||||
}
|
||||
|
||||
set streamline(streamline) {
|
||||
this.#streamline = streamline;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
#simulatePressure = this.getAttribute('streamline') === 'false' ? false : true;
|
||||
|
||||
get simulatePressure() {
|
||||
return this.#simulatePressure;
|
||||
}
|
||||
|
||||
set simulatePressure(simulatePressure) {
|
||||
this.#simulatePressure = simulatePressure;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
#points: Point[] = JSON.parse(this.getAttribute('points') || '[]');
|
||||
|
||||
get points() {
|
||||
return this.#points;
|
||||
}
|
||||
|
||||
set points(points) {
|
||||
this.#points = points;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true });
|
||||
shadowRoot.adoptedStyleSheets.push(styles);
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.appendChild(this.#path);
|
||||
shadowRoot.appendChild(svg);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.#update();
|
||||
}
|
||||
|
||||
trace() {
|
||||
this.points = [];
|
||||
this.#internals.states.add('tracing');
|
||||
this.addEventListener('pointerdown', this);
|
||||
this.addEventListener('lostpointercapture', this);
|
||||
}
|
||||
|
||||
addPoint(point: Point) {
|
||||
this.#points.push(point);
|
||||
this.#update();
|
||||
}
|
||||
|
||||
handleEvent(event: PointerEvent) {
|
||||
switch (event.type) {
|
||||
case 'pointerdown': {
|
||||
if (event.button !== 0 || event.ctrlKey) return;
|
||||
|
||||
this.addEventListener('pointermove', this);
|
||||
this.setPointerCapture(event.pointerId);
|
||||
this.addPoint([event.pageX, event.pageY, event.pressure]);
|
||||
return;
|
||||
}
|
||||
case 'pointermove': {
|
||||
this.addPoint([event.pageX, event.pageY, event.pressure]);
|
||||
return;
|
||||
}
|
||||
case 'lostpointercapture': {
|
||||
this.#internals.states.delete('tracing');
|
||||
this.removeEventListener('pointermove', this);
|
||||
this.removeEventListener('pointerdown', this);
|
||||
this.removeEventListener('lostpointercapture', this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#update() {
|
||||
const options: StrokeOptions = {
|
||||
size: this.#size,
|
||||
thinning: this.#thinning,
|
||||
smoothing: this.#smoothing,
|
||||
streamline: this.#streamline,
|
||||
simulatePressure: this.#simulatePressure,
|
||||
// TODO: figure out how to expose these as attributes
|
||||
easing: (t) => t,
|
||||
start: {
|
||||
taper: 0,
|
||||
easing: (t) => t,
|
||||
cap: true,
|
||||
},
|
||||
end: {
|
||||
taper: 100,
|
||||
easing: (t) => t,
|
||||
cap: true,
|
||||
},
|
||||
};
|
||||
|
||||
this.#stroke = getStroke(this.#points, options);
|
||||
this.#d = this.#getSvgPathFromStroke(this.#stroke);
|
||||
this.#path.setAttribute('d', this.#d);
|
||||
}
|
||||
|
||||
#getSvgPathFromStroke(stroke: Stroke): string {
|
||||
if (stroke.length === 0) return '';
|
||||
|
||||
const d = stroke.reduce(
|
||||
(acc, [x0, y0], i, arr) => {
|
||||
const [x1, y1] = arr[(i + 1) % arr.length];
|
||||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
|
||||
return acc;
|
||||
},
|
||||
['M', ...stroke[0], 'Q']
|
||||
);
|
||||
|
||||
d.push('Z');
|
||||
return d.join(' ');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue