1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-09-26 16:19:03 +02:00
Files
BezierInfo-2/docs/js/custom-element/api/graphics-api.js
Pomax 61bb4d00d9 .
2020-09-11 17:34:33 -07:00

914 lines
19 KiB
JavaScript

import { enrich } from "../lib/enrich.js";
import { create } from "../lib/create.js";
import { Bezier } from "./types/bezier.js";
import { BSpline } from "./types/bspline.js";
import { Vector } from "./types/vector.js";
import { Matrix } from "./types/matrix.js";
import { Shape } from "./util/shape.js";
import binomial from "./util/binomial.js";
import { BaseAPI } from "./base-api.js";
const MOUSE_PRECISION_ZONE = 5;
const TOUCH_PRECISION_ZONE = 30;
let CURRENT_HUE = 0;
/**
* Our Graphics API, which is the "public" side of the API.
*/
class GraphicsAPI extends BaseAPI {
static get constants() {
return [
`POINTER`,
`HAND`,
`PI`,
`TAU`,
`POLYGON`,
`CURVE`,
`BEZIER`,
`CENTER`,
`LEFT`,
`RIGHT`,
];
}
draw() {
CURRENT_HUE = 0;
super.draw();
}
get PI() {
return 3.14159265358979;
}
get TAU() {
return 6.28318530717958;
}
get POINTER() {
return `default`;
}
get HAND() {
return `pointer`;
}
get POLYGON() {
return Shape.POLYGON;
}
get CURVE() {
return Shape.CURVE;
}
get BEZIER() {
return Shape.BEZIER;
}
get CENTER() {
return `center`;
}
get LEFT() {
return `left`;
}
get RIGHT() {
return `right`;
}
// hatching patterns
get HATCH1() {
return this.HATCHING[0];
}
get HATCH2() {
return this.HATCHING[1];
}
get HATCH3() {
return this.HATCHING[2];
}
get HATCH4() {
return this.HATCHING[3];
}
get HATCH5() {
return this.HATCHING[4];
}
get HATCH6() {
return this.HATCHING[5];
}
onMouseDown(evt) {
super.onMouseDown(evt);
// mark this position as "when the cursor came down"
this.cursor.mark = { x: this.cursor.x, y: this.cursor.y };
// as well as for "what it was the previous cursor event"
this.cursor.last = { x: this.cursor.x, y: this.cursor.y };
const cdist = evt.targetTouches
? TOUCH_PRECISION_ZONE
: MOUSE_PRECISION_ZONE;
for (let i = 0, e = this.movable.length, p, d; i < e; i++) {
p = this.movable[i];
d = new Vector(p).dist(this.cursor);
if (d <= cdist) {
this.currentPoint = p;
break;
}
}
}
onMouseMove(evt) {
super.onMouseMove(evt);
if (this.cursor.down) {
// If we're click-dragging, or touch-moving, update the
// "since last event" as well as "compared to initial event"
// cursor positional differences:
this.cursor.diff = {
x: this.cursor.x - this.cursor.last.x,
y: this.cursor.y - this.cursor.last.y,
total: {
x: this.cursor.x - this.cursor.mark.x,
y: this.cursor.y - this.cursor.mark.y,
},
};
this.cursor.last = { x: this.cursor.x, y: this.cursor.y };
}
// Are we dragging a movable point around?
if (this.currentPoint) {
this.currentPoint.x = this.cursor.x;
this.currentPoint.y = this.cursor.y;
} else {
for (let i = 0, e = this.movable.length, p; i < e; i++) {
p = this.movable[i];
if (new Vector(p).dist(this.cursor) <= 5) {
this.setCursor(this.HAND);
return; // NOTE: this is a return, not a break!
}
}
this.setCursor(this.POINTER);
}
}
onMouseUp(evt) {
super.onMouseUp(evt);
delete this.cursor.mark;
delete this.cursor.last;
delete this.cursor.diff;
this.currentPoint = false;
}
setup() {
super.setup();
this.setGrid(20, `#F0F0F0`);
}
resetMovable(...allpoints) {
this.movable.splice(0, this.movable.length);
if (allpoints) this.setMovable(...allpoints);
}
setMovable(...allpoints) {
allpoints.forEach((points) => points.forEach((p) => this.movable.push(p)));
}
/**
* Multi-panel graphics: set panel count
*/
setPanelCount(c) {
this.panelWidth = this.width / c;
}
/**
* Multi-panel graphics: set up (0,0) to the next panel's start
*/
nextPanel(color = `black`) {
this.translate(this.panelWidth, 0);
this.setStroke(color);
this.line(0, 0, 0, this.height);
}
/**
* Dynamically add a slider
*/
addSlider(classes, propname, min, max, step, value, transform) {
if (this.element) {
let slider = create(`input`);
slider.type = `range`;
slider.min = min;
slider.max = max;
slider.step = step;
slider.setAttribute(`value`, value);
slider.setAttribute(`class`, classes);
this.element.append(slider);
this.setSlider(slider, propname, value, transform);
}
}
/**
* Update a slider with new min/max/step parameters, and value.
*
* @param {*} propname
* @param {*} min
* @param {*} max
* @param {*} step
* @param {*} value
*/
updateSlider(propname, min, max, step, value) {
let slider = this._sliders[propname];
if (!slider) {
throw new Error(`this.${propname} has no associated slider.`);
}
slider.setAttribute(`min`, min);
slider.setAttribute(`max`, max);
slider.setAttribute(`step`, step);
slider.setAttribute(`value`, value);
slider.updateProperty(value);
}
/**
* Set up a slider to control a named, numerical property in the sketch.
*
* @param {String} local query selector for the type=range element.
* @param {String} propname the name of the property to control.
* @param {float} initial the initial value for this property.
* @param {boolean} redraw whether or not to redraw after updating the value from the slider.
*/
setSlider(qs, propname, initial, transform) {
if (propname !== false && typeof this[propname] !== `undefined`) {
throw new Error(`this.${propname} already exists: cannot bind slider.`);
}
this._sliders = this._sliders || {};
let propLabel = propname.replace(`!`, ``);
propname = propLabel === propname ? propname : false;
let slider = typeof qs === `string` ? this.find(qs) : qs;
// relocate this slider
let ui = (() => {
if (!this.element) {
return { update: (v) => {} };
}
let wrapper = create(`div`);
wrapper.classList.add(`slider-wrapper`);
let label = create(`label`);
label.classList.add(`slider-label`);
label.innerHTML = propLabel;
wrapper.append(label);
slider.parentNode.replaceChild(wrapper, slider);
slider.classList.add(`slider`);
this._sliders[propname] = slider;
wrapper.append(slider);
let valueField = create(`label`);
valueField.classList.add(`slider-value`);
valueField.textContent;
wrapper.append(valueField);
return { update: (v) => (valueField.textContent = v) };
})();
if (!slider) {
console.warn(`Warning: no slider found for query selector "${qs}"`);
if (propname) this[propname] = initial;
return undefined;
}
slider.updateProperty = (evt) => {
let value = parseFloat(slider.value);
ui.update(value);
try {
let checked = transform ? transform(value) ?? value : value;
if (propname) this[propname] = checked;
} catch (e) {
if (evt instanceof Event) {
evt.preventDefault();
evt.stopPropagation();
}
ui.update(e.value);
slider.value = e.value;
slider.setAttribute(`value`, e.value);
}
if (!this.redrawing) this.redraw();
};
slider.value = initial;
slider.updateProperty({ target: { value: initial } });
slider.listen(`input`, (evt) => slider.updateProperty(evt));
return slider;
}
/**
* remove all sliders from this element
*/
removeSliders() {
this.findAll(`.slider-wrapper`).forEach((s) => {
s.parentNode.removeChild(s);
});
}
/**
* Convert the canvas to an image
*/
toDataURL() {
return this.canvas.toDataURL();
}
/**
* Draw an image onto the canvas
*/
image(img, x = 0, y = 0, w, h) {
this.ctx.drawImage(img, x, y, w || img.width, h || img.height);
}
/**
* transforms: translate
*/
translate(x, y) {
this.ctx.translate(x, y);
}
/**
* transforms: rotate
*/
rotate(angle) {
this.ctx.rotate(angle);
}
/**
* transforms: scale
*/
scale(x, y) {
y = y ? y : x; // NOTE: this turns y=0 into y=x, which is fine. Scaling by 0 is really silly =)
this.ctx.scale(x, y);
}
/**
* transforms: screen to world
*/
screenToWorld(x, y) {
if (y === undefined) {
y = x.y;
x = x.x;
}
let M = this.ctx.getTransform().invertSelf();
let ret = {
x: x * M.a + y * M.c + M.e,
y: x * M.b + y * M.d + M.f,
};
return ret;
}
/**
* transforms: world to screen
*/
worldToScreen(x, y) {
if (y === undefined) {
y = x.y;
x = x.x;
}
let M = this.ctx.getTransform();
let ret = {
x: x * M.a + y * M.c + M.e,
y: x * M.b + y * M.d + M.f,
};
return ret;
}
/**
* transforms: reset
*/
resetTransform() {
this.ctx.resetTransform();
}
/**
* custom element scoped querySelector
*/
find(qs) {
if (!this.element) return false;
return enrich(this.element.querySelector(qs));
}
/**
* custom element scoped querySelectorAll
*/
findAll(qs) {
if (!this.element) return false;
return Array.from(this.element.querySelectorAll(qs)).map((e) => enrich(e));
}
/**
* Set a (CSS) border on the canvas
*/
setBorder(width = 1, color = `black`) {
if (width === false) {
this.canvas.style.border = `none`;
} else {
this.canvas.style.border = `${width}px solid ${color}`;
}
}
/**
* Set the cursor type while the cursor is over the canvas
*/
setCursor(type) {
this.canvas.style.cursor = type;
}
/**
* Get a random color
*/
randomColor(a = 1.0, cycle = true) {
if (cycle) CURRENT_HUE = (CURRENT_HUE + 73) % 360;
return `hsla(${CURRENT_HUE},50%,50%,${a})`;
}
/**
* Set the context fillStyle
*/
setFill(color) {
if (color === false) color = `transparent`;
this.ctx.fillStyle = color;
}
/**
* Convenience alias
*/
noFill() {
this.setFill(false);
}
/**
* Set the context strokeStyle
*/
setStroke(color) {
if (color === false) color = `transparent`;
this.ctx.strokeStyle = color;
}
/**
* Convenience alias
*/
noStroke() {
this.setStroke(false);
}
/**
* stroke + fill
*/
setColor(color) {
this.setFill(color);
this.setStroke(color);
}
/**
* no stroke/fill
*/
noColor() {
this.noFill();
this.noStroke();
}
/**
* Set a text stroke/color
*/
setTextStroke(color, weight) {
this.textStroke = color;
this.strokeWeight = weight;
}
/**
* Do not use text stroking.
*/
noTextStroke() {
this.textStroke = false;
this.strokeWeight = false;
}
/**
* Set the line-dash pattern
*/
setLineDash(...values) {
this.ctx.setLineDash(values);
}
/**
* disable line-dash
*/
noLineDash() {
this.ctx.setLineDash([]);
}
/**
* Set the context lineWidth
*/
setWidth(width) {
this.ctx.lineWidth = width;
}
/**
* Set the font size
*/
setFontSize(px) {
this.font.size = px;
this.setFont();
}
/**
* Set the font weight (CSS name/number)
*/
setFontWeight(val) {
this.font.weight = val;
this.setFont();
}
/**
* Set the font family by name
*/
setFontFamily(name) {
this.font.family = name;
this.setFont();
}
setFont(font) {
font =
font || `${this.font.weight} ${this.font.size}px ${this.font.family}`;
this.ctx.font = font;
}
/**
* Set text shadow
*/
setShadow(color, px) {
this.ctx.shadowColor = color;
this.ctx.shadowBlur = px;
}
/**
* Disable text shadow
*/
noShadow() {
this.ctx.shadowColor = `transparent`;
this.ctx.shadowBlur = 0;
}
/**
* Cache all styling values
*/
cacheStyle() {
this.ctx.cacheStyle();
}
/**
* restore all previous styling values
*/
restoreStyle() {
this.ctx.restoreStyle();
}
/**
* set the default grid size and color
* @param {*} size
* @param {*} color
*/
setGrid(size, color) {
this._gridParams = { size, color };
}
/**
* disable drawing a grid when clearing.
*/
noGrid() {
this._gridParams = false;
}
/**
* Reset the canvas bitmap to a uniform color.
*/
clear(color = `white`, preserveTransforms = false) {
this.ctx.cacheStyle();
this.resetTransform();
this.ctx.fillStyle = color;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.restoreStyle();
if (!preserveTransforms) this.resetTransform();
if (this._gridParams) {
this.ctx.cacheStyle();
this.setStroke(this._gridParams.color);
this.translate(0.5, 0.5);
this.drawGrid(this._gridParams.size);
this.translate(-0.5, -0.5);
this.ctx.restoreStyle();
}
}
/**
* Draw a Point (or {x,y,z?} conformant) object on the canvas
*/
point(x, y) {
this.circle(x, y, 5);
}
/**
* Draw a line between two Points
*/
line(x1, y1, x2, y2) {
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
}
/**
* Draw a circle
*/
circle(x, y, r) {
this.arc(x, y, r, 0, this.TAU);
}
/**
* Draw a circular arc
*/
arc(x, y, r, s, e, cx = false, cy = false) {
this.ctx.beginPath();
if (cx !== false && cy != false) this.ctx.moveTo(cx, cy);
this.ctx.arc(x, y, r, s, e);
if (cx !== false && cy != false) this.ctx.moveTo(cx, cy);
this.ctx.fill();
this.ctx.stroke();
}
/**
* Draw text on the canvas
*/
text(str, x, y, alignment) {
if (y === undefined) {
y = x.y;
x = x.x;
}
const ctx = this.ctx;
ctx.cacheStyle();
if (alignment) {
ctx.textAlign = alignment;
}
if (this.textStroke) {
this.ctx.lineWidth = this.strokeWeight;
this.setStroke(this.textStroke);
this.ctx.strokeText(str, x, y);
}
this.ctx.fillText(str, x, y);
ctx.restoreStyle();
}
/**
* Draw a rectangle start with {p} in the upper left
*/
rect(x, y, w, h) {
this.ctx.fillRect(x, y, w, h);
this.ctx.strokeRect(x, y, w, h);
}
/**
* Draw a function plot from [start] to [end] in [steps] steps.
* Returns the plot shape so that it can be cached for redrawing.
*/
plot(fn, start = 0, end = 1, steps = 24, xscale = 1, yscale = 1) {
const interval = end - start;
this.start();
for (let i = 0, e = steps - 1, v; i < steps; i++) {
v = fn(start + (interval * i) / e);
this.vertex(v.x * xscale, v.y * yscale);
}
this.end();
return this.currentShape;
}
/**
* A signal for starting a complex shape
*/
start(type) {
this.currentShape = new Shape(type || this.POLYGON);
}
/**
* A signal for starting a new segment of a complex shape
*/
segment(type, factor) {
this.currentShape.addSegment(type, factor);
}
/**
* Add a plain point to the current shape
*/
vertex(x, y) {
this.currentShape.vertex({ x, y });
}
/**
* Draw a previously created shape
*/
drawShape(...shapes) {
shapes = shapes.map((s) => {
if (s instanceof Shape) return s;
return new Shape(this.POLYGON, undefined, s);
});
this.currentShape = shapes[0].copy();
for (let i = 1; i < shapes.length; i++) {
this.currentShape.merge(shapes[i]);
}
this.end();
this.STARTREPORTING = false;
}
/**
* A signal to draw the current complex shape
*/
end(close = false) {
this.ctx.beginPath();
let { x, y } = this.currentShape.first;
this.ctx.moveTo(x, y);
this.currentShape.segments.forEach((s) =>
this[`draw${s.type}`](s.points, s.factor)
);
if (close) this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
}
/**
* Polygon draw function
*/
drawPolygon(points) {
points.forEach((p) => this.ctx.lineTo(p.x, p.y));
}
/**
* Curve draw function, which draws a CR curve as a series of Beziers
*/
drawCatmullRom(points, f) {
// invent a virtual first and last point
const f0 = points[0],
f1 = points[1],
fn = f0.reflect(f1),
l1 = points[points.length - 2],
l0 = points[points.length - 1],
ln = l0.reflect(l1),
cpoints = [fn, ...points, ln];
// four point sliding window over the segment
for (let i = 0, e = cpoints.length - 3; i < e; i++) {
let [c1, c2, c3, c4] = cpoints.slice(i, i + 4);
let p2 = {
x: c2.x + (c3.x - c1.x) / (6 * f),
y: c2.y + (c3.y - c1.y) / (6 * f),
};
let p3 = {
x: c3.x - (c4.x - c2.x) / (6 * f),
y: c3.y - (c4.y - c2.y) / (6 * f),
};
this.ctx.bezierCurveTo(p2.x, p2.y, p3.x, p3.y, c3.x, c3.y);
}
}
/**
* Curve draw function, which assumes Bezier coordinates
*/
drawBezier(points) {
for (let i = 0, e = points.length; i < e; i += 3) {
let [p1, p2, p3] = points.slice(i, i + 3);
this.ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
}
/**
* Yield a snapshot of the current shape.
*/
save() {
return this.currentShape;
}
/**
* convenient grid drawing function
*/
drawGrid(division = 20) {
let w = this.panelWidth ?? this.width;
for (let x = (division / 2) | 0; x < w; x += division) {
this.line(x, 0, x, this.height);
}
for (let y = (division / 2) | 0; y < this.height; y += division) {
this.line(0, y, w, y);
}
}
/**
* convenient axis drawing function
*
* api.drawAxes("t",0,1, "S","0%","100%");
*
*/
drawAxes(hlabel, hs, he, vlabel, vs, ve, w, h) {
h = h || this.height;
w = w || this.width;
this.line(0, 0, w, 0);
this.line(0, 0, 0, h);
const hpos = 0 - 5;
this.text(`${hlabel}`, w / 2, hpos, this.CENTER);
this.text(hs, 0, hpos, this.CENTER);
this.text(he, w, hpos, this.CENTER);
const vpos = -10;
this.text(`${vlabel}\n`, vpos, h / 2, this.RIGHT);
this.text(vs, vpos, 0 + 5, this.RIGHT);
this.text(ve, vpos, h, this.RIGHT);
}
/**
* math functions
*/
floor(v) {
return Math.floor(v);
}
ceil(v) {
return Math.ceil(v);
}
round(v) {
return Math.round(v);
}
random(v = 1) {
return Math.random() * v;
}
abs(v) {
return Math.abs(v);
}
min(...v) {
return Math.min(...v);
}
max(...v) {
return Math.max(...v);
}
approx(v1, v2, epsilon = 0.001) {
return Math.abs(v1 - v2) < epsilon;
}
sin(v) {
return Math.sin(v);
}
cos(v) {
return Math.cos(v);
}
tan(v) {
return Math.tan(v);
}
sqrt(v) {
return Math.sqrt(v);
}
atan2(dy, dx) {
return Math.atan2(dy, dx);
}
pow(v, p) {
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,
p = v - s;
let r = ns + (p * i2) / i1;
if (constrain) return this.constrain(r, ns, ne);
return r;
}
constrain(v, s, e) {
return v < s ? s : v > e ? e : v;
}
dist(x1, y1, x2, y2) {
let dx = x1 - x2;
let dy = y1 - y2;
return this.sqrt(dx * dx + dy * dy);
}
}
export { GraphicsAPI, Bezier, BSpline, Vector, Matrix, Shape };