mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-26 09:44:32 +02:00
catmull-rom
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { enrich } from "../lib/enrich.js";
|
||||
import { Bezier } from "./types/bezier.js";
|
||||
import { Vector } from "./types/vector.js";
|
||||
import { Matrix } from "./types/matrix.js";
|
||||
import { Shape } from "./util/shape.js";
|
||||
import { Matrix } from "./util/matrix.js";
|
||||
import binomial from "./util/binomial.js";
|
||||
import { BaseAPI } from "./base-api.js";
|
||||
|
||||
const MOUSE_PRECISION_ZONE = 5;
|
||||
@@ -169,12 +170,18 @@ class GraphicsAPI extends BaseAPI {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
slider.value = initial;
|
||||
this[propname] = parseFloat(slider.value);
|
||||
|
||||
let handlerName = `on${propname[0].toUpperCase()}${propname
|
||||
.substring(1)
|
||||
.toLowerCase()}`;
|
||||
|
||||
if (this[handlerName]) {
|
||||
this[handlerName](initial);
|
||||
} else {
|
||||
slider.value = initial;
|
||||
}
|
||||
|
||||
slider.listen(`input`, (evt) => {
|
||||
this[propname] = parseFloat(evt.target.value);
|
||||
if (this[handlerName]) this[handlerName](this[propname]);
|
||||
@@ -725,6 +732,10 @@ class GraphicsAPI extends BaseAPI {
|
||||
return Math.pow(v, p);
|
||||
}
|
||||
|
||||
binomial(n, k) {
|
||||
return binomial(n, k);
|
||||
}
|
||||
|
||||
map(v, s, e, ns, ne, constrain = false) {
|
||||
const i1 = e - s,
|
||||
i2 = ne - ns,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Vector } from "./vector.js";
|
||||
import { Bezier as Original } from "../../lib/bezierjs/bezier.js";
|
||||
import { fitCurveToPoints } from "../util/fit-curve-to-points.js";
|
||||
|
||||
/**
|
||||
* A canvas-aware Bezier curve class
|
||||
@@ -23,6 +24,30 @@ class Bezier extends Original {
|
||||
return new Bezier(apiInstance, 110, 150, 25, 190, 210, 250, 210, 30);
|
||||
}
|
||||
|
||||
static fitCurveToPoints(apiInstance, points, tvalues) {
|
||||
if (!tvalues) {
|
||||
const D = [0];
|
||||
for (let i = 1; i < n; i++) {
|
||||
D[i] =
|
||||
D[i - 1] +
|
||||
dist(points[i - 1].x, points[i - 1].y, points[i].x, points[i].y);
|
||||
}
|
||||
const S = [],
|
||||
len = D[n - 1];
|
||||
D.forEach((v, i) => {
|
||||
S[i] = v / len;
|
||||
});
|
||||
tvalues = S;
|
||||
}
|
||||
|
||||
const bestFitData = fitCurveToPoints(points, tvalues),
|
||||
x = bestFitData.x,
|
||||
y = bestFitData.y,
|
||||
bpoints = x.map((r, i) => ({ x: r[0], y: y[i][0] }));
|
||||
|
||||
return new Bezier(apiInstance, bpoints);
|
||||
}
|
||||
|
||||
constructor(apiInstance, ...coords) {
|
||||
if (!apiInstance || !apiInstance.setMovable) {
|
||||
throw new Error(
|
||||
|
@@ -125,9 +125,33 @@ function transpose(M) {
|
||||
}
|
||||
|
||||
class Matrix {
|
||||
constructor(data) {
|
||||
constructor(n, m, data) {
|
||||
data = n instanceof Array ? n : data;
|
||||
this.data =
|
||||
data ?? [...new Array(n)].map((v) => [...new Array(m)].map((v) => 0));
|
||||
this.rows = this.data.length;
|
||||
this.cols = this.data[0].length;
|
||||
}
|
||||
setData(data) {
|
||||
this.data = data;
|
||||
}
|
||||
get(i, j) {
|
||||
return this.data[i][j];
|
||||
}
|
||||
set(i, j, value) {
|
||||
this.data[i][j] = value;
|
||||
}
|
||||
row(i) {
|
||||
return this.data[i];
|
||||
}
|
||||
col(i) {
|
||||
var d = this.data,
|
||||
col = [];
|
||||
for (let r = 0, l = d.length; r < l; r++) {
|
||||
col.push(d[r][i]);
|
||||
}
|
||||
return col;
|
||||
}
|
||||
multiply(other) {
|
||||
return new Matrix(multiply(this.data, other.data));
|
||||
}
|
21
docs/js/custom-element/api/util/binomial.js
Normal file
21
docs/js/custom-element/api/util/binomial.js
Normal file
@@ -0,0 +1,21 @@
|
||||
var binomialCoefficients = [[1], [1, 1]];
|
||||
|
||||
/**
|
||||
* ... docs go here ...
|
||||
*/
|
||||
function binomial(n, k) {
|
||||
if (n === 0) return 1;
|
||||
var lut = binomialCoefficients;
|
||||
while (n >= lut.length) {
|
||||
var s = lut.length;
|
||||
var nextRow = [1];
|
||||
for (var i = 1, prev = s - 1; i < s; i++) {
|
||||
nextRow[i] = lut[prev][i - 1] + lut[prev][i];
|
||||
}
|
||||
nextRow[s] = 1;
|
||||
lut.push(nextRow);
|
||||
}
|
||||
return lut[n][k];
|
||||
}
|
||||
|
||||
export default binomial;
|
86
docs/js/custom-element/api/util/fit-curve-to-points.js
Normal file
86
docs/js/custom-element/api/util/fit-curve-to-points.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Matrix } from "../types/matrix.js";
|
||||
import binomial from "./binomial.js";
|
||||
|
||||
/*
|
||||
We can form any basis matrix using a generative approach:
|
||||
|
||||
- it's an M = (n x n) matrix
|
||||
- it's a lower triangular matrix: all the entries above the main diagonal are zero
|
||||
- the main diagonal consists of the binomial coefficients for n
|
||||
- all entries are symmetric about the antidiagonal.
|
||||
|
||||
What's more, if we number rows and columns starting at 0, then
|
||||
the value at position M[r,c], with row=r and column=c, can be
|
||||
expressed as:
|
||||
|
||||
M[r,c] = (r choose c) * M[r,r] * S,
|
||||
|
||||
where S = 1 if r+c is even, or -1 otherwise
|
||||
|
||||
That is: the values in column c are directly computed off of the
|
||||
binomial coefficients on the main diagonal, through multiplication
|
||||
by a binomial based on matrix position, with the sign of the value
|
||||
also determined by matrix position. This is actually very easy to
|
||||
write out in code:
|
||||
*/
|
||||
function generateBasisMatrix(n) {
|
||||
const M = new Matrix(n, n);
|
||||
|
||||
// populate the main diagonal
|
||||
var k = n - 1;
|
||||
for (let i = 0; i < n; i++) {
|
||||
M.set(i, i, binomial(k, i));
|
||||
}
|
||||
|
||||
// compute the remaining values
|
||||
for (var c = 0, r; c < n; c++) {
|
||||
for (r = c + 1; r < n; r++) {
|
||||
var sign = (r + c) % 2 === 0 ? 1 : -1;
|
||||
var value = binomial(r, c) * M.get(r, r);
|
||||
M.set(r, c, sign * value);
|
||||
}
|
||||
}
|
||||
|
||||
return M;
|
||||
}
|
||||
|
||||
/**
|
||||
* ...docs go here...
|
||||
*/
|
||||
function formTMatrix(row, n) {
|
||||
let data = [];
|
||||
for (var i = 0; i < n; i++) {
|
||||
data.push(row.map((v) => v ** i));
|
||||
}
|
||||
const Tt = new Matrix(n, n, data);
|
||||
const T = Tt.transpose();
|
||||
return { T, Tt };
|
||||
}
|
||||
|
||||
/**
|
||||
* ...docs go here...
|
||||
*/
|
||||
function computeBestFit(points, n, M, S) {
|
||||
var tm = formTMatrix(S, n),
|
||||
T = tm.T,
|
||||
Tt = tm.Tt,
|
||||
M1 = M.invert(),
|
||||
TtT1 = Tt.multiply(T).invert(),
|
||||
step1 = TtT1.multiply(Tt),
|
||||
step2 = M1.multiply(step1),
|
||||
X = new Matrix(points.map((v) => [v.x])),
|
||||
Cx = step2.multiply(X),
|
||||
Y = new Matrix(points.map((v) => [v.y])),
|
||||
Cy = step2.multiply(Y);
|
||||
return { x: Cx.data, y: Cy.data };
|
||||
}
|
||||
|
||||
/**
|
||||
* ...docs go here...
|
||||
*/
|
||||
function fitCurveToPoints(points, tvalues) {
|
||||
const n = points.length;
|
||||
return computeBestFit(points, n, generateBasisMatrix(n), tvalues);
|
||||
}
|
||||
|
||||
export { fitCurveToPoints };
|
@@ -35,10 +35,8 @@ graphics-element:not(:defined) > * {
|
||||
|
||||
graphics-element:not(:defined) fallback-image {
|
||||
display: block;
|
||||
font-size: 60%;
|
||||
text-align: center;
|
||||
padding-bottom: 0.2em;
|
||||
visibility:collapse;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -46,10 +44,25 @@ graphics-element:not(:defined) fallback-image {
|
||||
treated as a full block, so that the caption text shows up underneath
|
||||
it, rather than next to it:
|
||||
*/
|
||||
graphics-element:not(:defined) fallback-image > .view-source {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -0.6em;
|
||||
margin-bottom: -0.2em;
|
||||
font-size: 60%;
|
||||
color: #00000030;
|
||||
}
|
||||
|
||||
graphics-element:not(:defined) fallback-image > label {
|
||||
display: block;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
graphics-element:not(:defined) fallback-image > img {
|
||||
display: block;
|
||||
visibility:visible;
|
||||
border: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -6,6 +6,20 @@ import performCodeSurgery from "./lib/perform-code-surgery.js";
|
||||
const MODULE_URL = import.meta.url;
|
||||
const MODULE_PATH = MODULE_URL.slice(0, MODULE_URL.lastIndexOf(`/`));
|
||||
|
||||
// Really wish this was baked into the DOM API...
|
||||
function isInViewport(e) {
|
||||
if (typeof window === `undefined`) return true;
|
||||
if (typeof document === `undefined`) return true;
|
||||
|
||||
var b = e.getBoundingClientRect();
|
||||
return (
|
||||
b.top >= 0 &&
|
||||
b.left >= 0 &&
|
||||
b.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
b.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple "for programming code" element, for holding entire
|
||||
* programs, rather than code snippets.
|
||||
@@ -18,21 +32,35 @@ CustomElement.register(class ProgramCode extends HTMLElement {});
|
||||
class GraphicsElement extends CustomElement {
|
||||
static DEBUG = false;
|
||||
|
||||
/**
|
||||
* Create an instance of this element
|
||||
*/
|
||||
constructor() {
|
||||
super({ header: false, footer: false });
|
||||
|
||||
// Strip out fallback images: if we can get here,
|
||||
// we should not be loading fallback images because
|
||||
// we know we're showing live content instead.
|
||||
let fallback = this.querySelector(`fallback-image`);
|
||||
if (fallback) this.removeChild(fallback);
|
||||
|
||||
this.loadSource();
|
||||
|
||||
if (this.title) {
|
||||
this.label = document.createElement(`label`);
|
||||
this.label.textContent = this.title;
|
||||
// Do we load immediately?
|
||||
if (isInViewport(this)) {
|
||||
this.loadSource();
|
||||
}
|
||||
|
||||
// Or do we load later, once we've been scrolled into view?
|
||||
else {
|
||||
let fallback = this.querySelector(`img`);
|
||||
new IntersectionObserver(
|
||||
(entries, observer) =>
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.loadSource();
|
||||
observer.disconnect();
|
||||
}
|
||||
}),
|
||||
{ threshold: 0.1, rootMargin: `${window.innerHeight}px` }
|
||||
).observe(fallback);
|
||||
}
|
||||
|
||||
this.label = document.createElement(`label`);
|
||||
if (!this.title) this.title = ``;
|
||||
this.label.textContent = this.title;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +81,7 @@ class GraphicsElement extends CustomElement {
|
||||
* part of the CustomElement API
|
||||
*/
|
||||
handleChildChanges(added, removed) {
|
||||
// console.log(`child change:`, added, removed);
|
||||
// debugLog(`child change:`, added, removed);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,6 +108,8 @@ class GraphicsElement extends CustomElement {
|
||||
* Load the graphics code, either from a src URL, a <program-code> element, or .textContent
|
||||
*/
|
||||
async loadSource() {
|
||||
debugLog(`loading ${this.getAttribute(`src`)}`);
|
||||
|
||||
let src = false;
|
||||
let codeElement = this.querySelector(`program-code`);
|
||||
|
||||
@@ -189,6 +219,7 @@ class GraphicsElement extends CustomElement {
|
||||
script.src = `data:application/javascript;charset=utf-8,${encodeURIComponent(
|
||||
this.code
|
||||
)}`;
|
||||
|
||||
if (rerender) this.render();
|
||||
}
|
||||
|
||||
@@ -198,6 +229,9 @@ class GraphicsElement extends CustomElement {
|
||||
setGraphic(apiInstance) {
|
||||
this.apiInstance = apiInstance;
|
||||
this.setCanvas(apiInstance.canvas);
|
||||
// at this point we can remove our placeholder image for this element, too.
|
||||
let fallback = this.querySelector(`fallback-image`);
|
||||
if (fallback) this.removeChild(fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,14 +256,12 @@ class GraphicsElement extends CustomElement {
|
||||
* can't actually find anywhere in the document or shadow DOM...
|
||||
*/
|
||||
printCodeDueToError() {
|
||||
if (GraphicsElement.DEBUG) {
|
||||
console.log(
|
||||
this.code
|
||||
.split(`\n`)
|
||||
.map((l, pos) => `${pos + 1}: ${l}`)
|
||||
.join(`\n`)
|
||||
);
|
||||
}
|
||||
debugLog(
|
||||
this.code
|
||||
.split(`\n`)
|
||||
.map((l, pos) => `${pos + 1}: ${l}`)
|
||||
.join(`\n`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,4 +316,11 @@ if (typeof window !== undefined) {
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// debugging should be behind a flag
|
||||
function debugLog(...data) {
|
||||
if (GraphicsElement.DEBUG) {
|
||||
console.log(...data);
|
||||
}
|
||||
}
|
||||
|
||||
export { GraphicsElement };
|
||||
|
Reference in New Issue
Block a user