mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-27 18:20:24 +02:00
renamed graphics-element dir
This commit is contained in:
428
docs/js/graphics-element/api/README.md
Normal file
428
docs/js/graphics-element/api/README.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# The Graphics API
|
||||
|
||||
Graphics code is bootstrapped and drawn uses two "master" functions:
|
||||
|
||||
```js
|
||||
setup() {
|
||||
// initialisation code goes here
|
||||
}
|
||||
|
||||
draw() {
|
||||
// drawing code goes here
|
||||
}
|
||||
```
|
||||
|
||||
All code starts at `setup()`, automatically calling `draw()` after setup has been completed. Standard JS scoping applies, so any variable declared outside of `setup`/`draw` will be a "global" variable.
|
||||
|
||||
Note that neither of these functions are _required_: without a `setup()` function the code will just jump straight to `draw()`, and without a `draw()` function the code will simply not draw anything beyond the initial empty canvas.
|
||||
|
||||
## User-initiated events
|
||||
|
||||
### Touch/mouse events
|
||||
|
||||
Graphics code can react to touch/mouse, which can be handled using:
|
||||
|
||||
- `onMouseDown()` triggered by mouse/touch start events.
|
||||
- `onMouseUp()` triggered by mouse/touch end events.
|
||||
- `onMouseMove()` triggered by moving the mouse/your finger.
|
||||
|
||||
Mouse event data can be accessed via the `this.cursor` property, which encodes:
|
||||
|
||||
```
|
||||
{
|
||||
x: current event's screen x coordinate
|
||||
y: current event's screen x coordinate
|
||||
down: boolean signifying whether the cursor is engaged or not
|
||||
mark: {x,y} coordinate object representing where mousedown occurred
|
||||
last: {x,y} coordinate object representing where the cursor was "one event ago"
|
||||
diff: {x,y} coordinate object representing the x/y difference between "now" and "one event ago",
|
||||
with an additional `total` propert that is an {x,y} coordinate object representing the x/y
|
||||
difference between "now" and the original mousedown event.
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
setup() {
|
||||
this.defaultBgColor = this.bgColor = `green`;
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear(this.bgColor);
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
this.bgColor = `blue`;
|
||||
redraw();
|
||||
}
|
||||
|
||||
onMouseMove() {
|
||||
this.bgColor = `red`;
|
||||
redraw();
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
this.bgColor = this.defaultBgColor;
|
||||
redraw();
|
||||
}
|
||||
```
|
||||
|
||||
### Keyboard events
|
||||
|
||||
Graphics code can also react to keyboard events, although this is a great way to make sure your code won't work for mobile devices, so it's better to use range sliders to keep things accessible. That said, they can be handled using:
|
||||
|
||||
- `onKeyDown()` triggered by pressing a key
|
||||
- `onKeyUp()` triggered by releasing a key
|
||||
|
||||
Keyboard event data can be accessed via the `this.keyboard` property, which encodes:
|
||||
|
||||
```
|
||||
{
|
||||
currentKey: the name of the key associated with the current event
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, the `this.keyboard` property can be consulted for named keys to see if they are currently down or not, e.g. to check whether the up arrow is down or not:
|
||||
|
||||
```js
|
||||
draw() {
|
||||
if (this.keyboard[`w`] && this.keyboard[`d`]) {
|
||||
// move up-left
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
setup() {
|
||||
this.y = this.height/2;
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
setColor(`black`);
|
||||
rect(0, this.y-1, this.width, 3);
|
||||
}
|
||||
|
||||
onKeyDown() {
|
||||
const key = this.keyboard.currentKey;
|
||||
if (key === `ArrowUp`) {
|
||||
y -= 5
|
||||
}
|
||||
if (key === `ArrowDown`) {
|
||||
y += 5;
|
||||
}
|
||||
y = constrain(y, 0, this.height);
|
||||
redraw();
|
||||
}
|
||||
```
|
||||
|
||||
## Controllable parameters
|
||||
|
||||
Graphics code can be provided with outside values in two different ways.
|
||||
|
||||
### Using fixed startup parameters
|
||||
|
||||
Graphics code can be passed fixed values from HTML using data attributes:
|
||||
|
||||
```html
|
||||
<graphics-element src="..." data-propname="somevalue"></graphics-element>
|
||||
```
|
||||
|
||||
which can be access on the code side using
|
||||
|
||||
```js
|
||||
this.parameters.propname;
|
||||
```
|
||||
|
||||
Note that `this.parameters` is a protected object. Properties of the parameters object can be updated by your code, but you cannot reassign `this.parameters` itself.
|
||||
|
||||
|
||||
### Using dynamic value sliders
|
||||
|
||||
Graphics code has also be provided with dynamic values by using range sliders. There are two ways to do this: purely in code, or by tying the graphics code to HTML sliders.
|
||||
|
||||
#### In code
|
||||
|
||||
If sliders may be dynamically required, the `addSlider` function can be used:
|
||||
|
||||
```js
|
||||
setup() {
|
||||
addSlider(`rangeValue`, 0, 1, 0.001, 0.5);
|
||||
}
|
||||
|
||||
draw() {
|
||||
console.log(this.rangeValue);
|
||||
}
|
||||
```
|
||||
|
||||
Its function signature is `addSlider(property name, min, max, step, initial value)`, in which the arguments represent:
|
||||
|
||||
- `property name` a propertyname string that may start with `!`. If no `!` is used, the property name should follow the rules for variable names, as the property will be exposed as `this.propertyname` (e.g. is you use `rangeValue`, then `this.rangeValue` will exis and be kept up to date by the slider logic). If `!` is used, no `this.propertyname` will be be set up for use in your code. Regardless of whether `!` is used or not, the property name will also be displayed in the slider's UI.
|
||||
- `min` the minimum numerical value this variable will be able to take on
|
||||
- `max` the meximum numerical value this variable will be able to take on
|
||||
- `step` the value increase/decrease per step of the slider.
|
||||
- `initial value` the value that the associated variable will be assigned as part of the `addSlider` call.
|
||||
|
||||
#### From HTML
|
||||
|
||||
You can also "presupply" a graphic with sliders, if you know your graphic has a fixed number of dynamic variables. This uses the standard HTML `<input type="range">` element:
|
||||
|
||||
```html
|
||||
<graphics-element src="..." data-propname="somevalue">
|
||||
<input type="range" min="0" max="1" step="0.001" value="0.5" class="my-slider">
|
||||
</graphics-element>
|
||||
```
|
||||
|
||||
With the graphic code using `setSlider` with a query selector to find your slider element and tie it to a variable:
|
||||
|
||||
```js
|
||||
setup() {
|
||||
setSlider(`.my-slider`, `rangeValue`, 0.5);
|
||||
}
|
||||
|
||||
draw() {
|
||||
console.log(this.rangeValue);
|
||||
}
|
||||
```
|
||||
|
||||
Its function signature is `setSlider(query selector, property name, initial value)`, in which the arguments represent:
|
||||
|
||||
- `query select` a CSS query selector for finding the right slider in your `<graphics-element>` tree. If you only have one slider then this query selector can simply be `input[type=range]`, but if you have multiple sliders it's a better idea to give each slider a CSS class that can be used to indentify it.
|
||||
- `property name` a propertyname string that may start with `!`. If no `!` is used, the property name should follow the rules for variable names, as the property will be exposed as `this.propertyname` (e.g. is you use `rangeValue`, then `this.rangeValue` will exis and be kept up to date by the slider logic). If `!` is used, no `this.propertyname` will be be set up for use in your code. Regardless of whether `!` is used or not, the property name will also be displayed in the slider's UI.
|
||||
- `initial value` the value that the associated variable will be assigned as part of the `addSlider` call.
|
||||
|
||||
Note that while it might seem that `<input>` elements can be made fully self-descriptive for both the property name (using the `name` attribute) and initial value (using the `value` attribute), this code still needs to do the right thing even in the absence of an HTML page, and so the property name and initial value are explicitly required.
|
||||
|
||||
**warning:** if you try to set up a slider for a property name that you have already defined, the code will throw a runtime error.
|
||||
|
||||
|
||||
## Movable points
|
||||
|
||||
An important part of the Graphics API is showing shapes that are controlled or defined by coordinates, and as there are special functions for marking points as "movable" - that is, these points can be click/touch-dragged around a graphic. To fascilitate this, the following functions can be used:
|
||||
|
||||
- `setMovable(points, ...)` takes one or more arrays of points, and marks all points as "being movable", such that if the cursor activates at an x/y coordinate near one of these, that point gets assigned to `this.currentPoint`, as well as being automatically moved around as you drag the cursor around on the sketch.
|
||||
- `resetMovable()` will clear the list of movable points.
|
||||
- `resetMovable(points, ...)` is the same as calling `resetMovable()` followed by `setMovable(points, ...)`.
|
||||
|
||||
|
||||
## The API
|
||||
|
||||
The following is the list of API functions that can be used to draw... whatever you like, really.
|
||||
|
||||
### Global constants
|
||||
|
||||
- `PI` 3.14159265358979
|
||||
- `TAU` 6.28318530717958
|
||||
- `POINTER` "default"
|
||||
- `HAND` "pointer"
|
||||
- `CROSS` "crosshair"
|
||||
- `POLYGON` Shape.POLYGON, "Polygon"
|
||||
- `CURVE` Shape.CURVE, "CatmullRom"
|
||||
- `BEZIER` Shape.BEZIER, "Bezier"
|
||||
- `CENTER` "center"
|
||||
- `LEFT` "left"
|
||||
- `RIGHT` "right"
|
||||
|
||||
|
||||
### Instance propeties
|
||||
|
||||
- `this.width` the width of the graphic
|
||||
- `this.height` the height of the graphic
|
||||
- `this.frame` the current frame (i.e. the number of times `draw()` has run)
|
||||
- `this.panelWidth` the width of a single panel in the graphic, only meaningful in conjunction with `setPanelWidth` (see below)
|
||||
- `this.parameters` the collection of externally passed parameters (via HTML: `data-...` attributes, via JS: a key/value object)
|
||||
- `this.cursor` represents the current mouse/touch cursor state
|
||||
- `this.currentPoint` whatever point thec cursor is currently close enough to to interact with
|
||||
- `this.keyboard` the current keyboard state
|
||||
- `this.currentShape` the currently active shape. **warning:** this value gets reset any time `start()` is used, so it is recommended to cache the current shape using `saveShape()` instead of directly referencing `this.currentShape`.
|
||||
|
||||
|
||||
### General functions
|
||||
|
||||
- `setSize(width,height)` explicitly resizes the canvas. **warning:** this will reset all color, transform, etc. properties to their default values.
|
||||
- `setPanelCount(int)` use this in `setup()` to let the API know that this graphic is technically a number of "separate" panels of content, setting `this.panelWidth` to `width`/`panelcount`.
|
||||
- `toDataURL()` returns the graphic as PNG image, encoded as a data URL.
|
||||
- `find(qs)` find an HTML elements in the `<graphics-element>` DOM tree, using a query selector
|
||||
- `findAll(qs)` find all HTML elements that match the provided querySelector. **note:** unlike the DOM API, this function returns a plain array.
|
||||
|
||||
|
||||
### Maths functions
|
||||
|
||||
- `abs(v)` get the absolute value
|
||||
- `approx(v1, v2, epsilon = 0.001)` check whether v1 differs from v2 by no more than `epsilon`
|
||||
- `atan2(dy, dx)` [atan2](https://en.wikipedia.org/wiki/Atan2)
|
||||
- `binomial(n, k)` get the binomial coefficient, i.e. "n choose k"
|
||||
- `ceil(v)` round any fractional number up to the next highest interger
|
||||
- `constrain(v, lowest, highest)` restrict a value in its lowest and highest value.
|
||||
- `cos(v)` cosine
|
||||
- `dist(x1, y1, x2, y2)` the euclidean distance between (x1,y1) and (x2,y2)
|
||||
- `floor(v)` round any fractional number to an integer by discarding its fractional part
|
||||
- `map(v, fromStart, fromEnd, toStrart, toEnd, constrain = false)` compute a value on an interval [fromStart,fromEnd] to its corresponding value on the interval [toStart,toEnd], with optional constraining to that new interval.
|
||||
- `max(...v)` find the highest number in two or more numbers
|
||||
- `min(...v)` find the lowest number in two or more numbers
|
||||
- `random()` generate a random value between 0 (inclusive) and 1 (exclusive)
|
||||
- `random(v)` generate a random value between 0 (inclusive) and `v` (exclusive)
|
||||
- `random(a,b)` generate a random value between `a` (inclusive) and `b` (exclusive)
|
||||
- `round(v)` round any fractional number by applying `ceil` for any number with fractional part >= 0.5, and `floor` for any number with fractional part < 0.5.
|
||||
- `sin(v)` sine
|
||||
- `sqrt(v)` square root
|
||||
- `tan(v)` tangent
|
||||
|
||||
### Property functions
|
||||
|
||||
- `setBorder(width = 1, color = "black")` set the canvas border width and color
|
||||
- `setColor(color)` set the color for both shape stroke and fill
|
||||
- `noColor()` set both stroke and fill color to "transparent"
|
||||
- `setCursor(type)` set the CSS cursor type. `POINTER`, `HAND`, and `CROSS` constants are provided, other values must be supplied as string.
|
||||
- `setFill(color)` set the fill color
|
||||
- `noFill()` set the fill color to "transparent"
|
||||
- `setFont(font)` set the text font, using [CSS font syntax](https://developer.mozilla.org/en-US/docs/Web/CSS/font)
|
||||
- `setFontFamily(name)` set the font to be used, by name
|
||||
- `setFontSize(px)` set the font size in pixels
|
||||
- `setFontWeight(val)` set the font weight in CSS weight units
|
||||
- `setGrid(size, color)` set the background grid's spacing and line coloring
|
||||
- `noGrid()` do not draw a background grid
|
||||
- `setLineDash(...values)` set the interval values for [dashed lines](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash)
|
||||
- `noLineDash()` do not use line dashing for strokes
|
||||
- `setShadow(color, px)` set the color and blur distance for drawing shape shadows
|
||||
- `noShadow()` do not use shape shadows
|
||||
- `setStroke(color)` set the stroke color
|
||||
- `noStroke()` set the stroke color to "transparent"
|
||||
- `setTextStroke(color, weight)` set the text outline stroke color and width (in pixels)
|
||||
- `noTextStroke()` disable text outline
|
||||
- `setWidth()` reset the stroke width to be 1 pixel wide
|
||||
- `setWidth(width)` set the stroke width to a custom value, in pixels
|
||||
|
||||
For coloring purposes, there is also the `randomColor` function:
|
||||
|
||||
- `randomColor()` returns a random, full opaque CSS color
|
||||
- `randomColor(opacity)` returns a random CSS color with the indicated opacity (in interval [0,1])
|
||||
- `randomColor(opacity, cycle=false)` if the second parameter is explicitly set to `false`, the random seed used to generate the random color will not be updated, and the resulting random color will be the same as when the function was previously called.
|
||||
|
||||
For temporary work, where you might want to change some properties and then revert to the previous state, there are two functions available:
|
||||
|
||||
- `save()` cache the current collection of properties. This uses a stack, with each call adding a new "snapshot" on the stack.
|
||||
- `restore()` restore the most recently cached state from the stack.
|
||||
|
||||
### Coordinate transform function
|
||||
|
||||
- `rotate(angle)` rotate the coordinate system by `angle` (clockwise, in radians)
|
||||
- `scale(x, y)` scale the coordinate system by a factor of `x` horizontally, and `y` vertically
|
||||
- `translate(x, y)` move the coordinate system by `x` units horizontally, and `y` units vertically
|
||||
- `resetTransform()` reset the coordinate system to its default values
|
||||
- `transform(a,b,c,d,e,f)` transform the coordinate system by applying a transformation matrix to it. This matrix has the form:
|
||||
|
||||
```
|
||||
| a b c |
|
||||
m = | d e f |
|
||||
| 0 0 1 |
|
||||
```
|
||||
|
||||
**note:** all transforms are with respect to (0,0) irrespective of where (0,0) has been moved to through successive transforms.
|
||||
|
||||
In addition to transformations, there are also two functions to allow you to map screen coordinates (e.g. cursor position) to their corresponding transformed coordinate, and vice versa, to allow for drawing/computing points in either coordinate system
|
||||
|
||||
- `screenToWorld(x, y)` converts a screen coordinate (x,y) to its corresponding transformed coordinate system coordinate (x',y')
|
||||
- `worldToScreen(x, y)` converts a transformed coordinate system coordinate (x,y) to its corresponding screen coordinate (x', y')
|
||||
|
||||
### Drawing functions
|
||||
|
||||
- `arc(x, y, r, s, e)` draw a section of outline for a circle with radius `r` centered on (x,y), starting at angle `s` and ending at angle `e`.
|
||||
- `circle(x, y, r)` draw a circle at (x,y) with radius `r`
|
||||
- `clear(color="white", preserveTransforms=false)` clears the graphics to a specific CSS background color, resetting the transform by default.
|
||||
- `drawAxes(hlabel, hs, he, vlabel, vs, ve, w, h)` draw a set of labelled axes, using `{hlabel, hs, he}` as horizontal label, with start marker `hs` and end marker `he`, and using `{vlabel, vs, ve}` as vertical label, with start marker `vs` and end marker `ve`
|
||||
- `drawGrid(division = 20)` draw a grid with the specified spacing, colored using the current stroke color
|
||||
- `drawShape(...shapes)` draw one or more saved full shapes (see below)
|
||||
- `image(img, x = 0, y = 0, w = auto, h = auto)` draw an image at some (x,y) coordinate, defaulting to (0,0), scaled to some width and height, defaulting to the native image dimensions
|
||||
- `line(x1, y1, x2, y2)` draw a line from (x1,y1) to (x2,y2)
|
||||
- `plot(fn, start = 0, end = 1, steps = 24, xscale = 1, yscale = 1)` plot a function defined by `fn` (which must take a single numerical input, and output an object of the form `{x:..., y:...}`) for inputs in the interval [start,end], at a resolution of `steps` steps, with the resulting values scaled by `xscale` horizontally, and `yscale` vertically. This function returns the plot as a `Shape` for later reuse.
|
||||
- `point(x, y)` draw a single point at (x,y)
|
||||
- `rect(x, y, w, h)` draw a rectangle from (x,y) with width `w` and height `h`
|
||||
- `redraw()` triggers a new draw loop. Use this instead of calling `draw()` directly when you wish to draw a new frame as part of event handling.
|
||||
- `text(str, x, y, alignment = LEFT)` place text, colored by the fill color, anchored to (x,y), with the type of anchoring determined by `alignemtn`. The alignment constants `LEFT`, `RIGHT`, and `CENTER` are available.
|
||||
- `wedge(x, y, r, s, e)` similar to arc, but draw a full wedge
|
||||
|
||||
#### Shape drawing functions
|
||||
|
||||
- `start(type = POLYGON, factor)` set up a new `Shape` as `this.currentShape` in preparation for receiving data. Types can be `POLYGON` (default), `CURVE` (Catmull-Rom), or `BEZIER`. If `CURVE` is specified, the `factor` indicates how loose or tight the resulting Catmull-Rom curve will be.
|
||||
- `segment(type, factor)` set up a new section of the shape. If left unspecified the `type` and `factor` are inherited from the current `Shape`.
|
||||
- `vertex(x, y)` add a point to the current `Shape`'s current segment.
|
||||
- `end(close = false)` draw the current `Shape`
|
||||
- `saveShape()` returns the current shape for later reuse
|
||||
|
||||
|
||||
## Built-in object types
|
||||
|
||||
### Shape
|
||||
|
||||
The `Shape` class reprents a drawable shape consisting of one or more polygonal, Catmull-Rom curve, or Bezier curve segments. It has a minimal API:
|
||||
|
||||
- **`new Shape(type, factor, points = [])`** construct a new `Shape` of the specified `type` (options are `Shape.POLYGON`, `Shape.CURVE` for Catmull-Rom curves, and `Shape.BEZIER` for Bezier curves), with the specified `factor` (used for Catmull-Rom curve tightness), and optional list of points to prepopulate the first shape segment.
|
||||
- `merge(other)` merge another `Shape`'s segments into this `Shape`
|
||||
- `copy()` returns a copy of this `Shape`
|
||||
- `newSegment(type, factor)` start a new segment in this `Shape` of the indicated type, with the indicated tightness factor.
|
||||
- `vertex(p)` add an on-shape coordinate (x,y) to this `Shape`. How this vertex contributes to the overall shape drawing depends on the current segment type.
|
||||
|
||||
|
||||
### Vector
|
||||
|
||||
The `Vector` class represents a 2d/3d coordinate, with a minimal standard API:
|
||||
|
||||
- **`new Vector(x,y,z?) / new Vector({x:,y:,z:}`** construct a new `Vector`
|
||||
- `vector.dist(other, y, z = 0)` calculate the distance to some other vector-as-coordinate
|
||||
- `vector.normalize(f)` return a new `Vector` representing a scaled copy of this vector, with length 1.0
|
||||
- `vector.getAngle()` get the angle between this vector and the x-axis.
|
||||
- `vector.reflect(other)` reflect this vector-as-coordinate over the line that some other vector lies on
|
||||
- `vector.add(other)` return a new `vector` representing the addition of some other vector to this vector
|
||||
- `vector.subtract(other)` return a new `vector` representing the subtraction of some other vector to this vector
|
||||
- `vector.scale(f = 1)` return a new `vector` representing the scaled version of this vector
|
||||
|
||||
|
||||
### Matrix
|
||||
|
||||
The `Matrix` class represents an `N`x`M` matrix, with minimal standard API:
|
||||
|
||||
- **`new Matrix(n,m,data?)`** construct a new `Matrix`. If `data` is provided, the matrix will be filled with that. **warning:** `data` is assumed to be `n` x `m`, but is **not** validated.
|
||||
- `setData(data)` **warning:** `data` is assumed to be `n` x `m`, but is **not** validated.
|
||||
- `get(i, j)` get the value at row `i` and column `j`
|
||||
- `set(i, j, value)` set the value at row `i` and column `j`
|
||||
- `row(i)` get the entire `i`th row as an array of values
|
||||
- `col(j)` get the entire `j`th column as a (flat) array of values
|
||||
- `multiply(other)` return a new `Matrix` representing the right-multiplication of this matrix with some other matrix
|
||||
- `invert()` return a new `Matrix` representing the inverse of this matrix, or `undefined` if no inverse exists
|
||||
- `transpose()` return a new `Matrix` representing the transpose of this matrix
|
||||
...
|
||||
|
||||
### Bezier
|
||||
|
||||
The `Bezier` class is an instance of [bezier.js](https://pomax.github.io/bezierjs/) with all its API functions, extended for use on the canvas:
|
||||
|
||||
- static `defaultQuadratic(apiInstance)` returns a new quadratic `Bezier` with preset coordinate values tailored to the Primer on Bezier Curves. The `apiInstance` must be a reference to a valid Graphics-API instance (typically thiat will simply be `this` in your code).
|
||||
- static `defaultCubic(apiInstance)` returns a new cubic `Bezier` with preset coordinate values tailored to the Primer on Bezier Curves. The `apiInstance` must be a reference to a valid Graphics-API instance (typically thiat will simply be `this` in your code).
|
||||
- static `fitCurveToPoints(apiInstance, points, tvalues)` returns a new `n`-dimensional `Bezier` that has been fit to the provided list of points, constrained to the provided `tvalues`, using MSE polygonal curve fitting. The `apiInstance` must be a reference to a valid Graphics-API instance (typically thiat will simply be `this` in your code).
|
||||
|
||||
|
||||
The extended API in addition to these static functions are:
|
||||
|
||||
- **`new Bezier(apiInstance, ...coords)`** construct a new `Bezier` curve controlled by three or more points, either supplied as numerical arguments, or as point objects `{x:..., y:..., z?:...}` where `z` is optional. The `apiInstance` must be a reference to a valid Graphics-API instance (typically thiat will simply be `this` in your code).
|
||||
- `project(x, y)` returns an `{x:..., y:...}` object representing the projection of some point (x,y) onto this curve.
|
||||
- `getPointNear(x, y, d = 5)` returns either `undefined`, or one of the `Bezier` curve's control points if the specified (x,y) coordinate is `d` or fewer pixels from a control point.
|
||||
- `drawCurve(color = #333)` draws this curve on the canvas, with optional custom color
|
||||
- `drawPoints(labels = true)` draws the curve's control points, with optional coordinate labels (defaulting to true)
|
||||
- `drawSkeleton(color = #555)` draws this curve's coordinate polygon, with optional custom color
|
||||
- `getStrutPoints(t)` get the list of points obtained through "de Casteljau" interpolation, for a given `t` value
|
||||
- `drawStruts(t, color = "black", showpoints = true)` draws this curve's "de Casteljau" points and lines, for a given `t` value, with optional custom color, and optional omission of the points if only the lines are required.
|
||||
- `drawBoundingBox(color = "black")` draw the axis-aligned bounding box for this `Bezier` curve with optional custom color
|
||||
|
||||
### BSpline
|
||||
|
||||
The `BSpline` class represents a generaic [B-spline](https://en.wikipedia.org/wiki/B-spline) curve, with a minimal API:
|
||||
|
||||
- **`new BSpline(apiInstance, points)`** constructs a B-spline controlled by a points array in which each element may be either of the form `{x: ..., y: ...}` or a numerical tuple `[x,y]`. The `apiInstance` must be a reference to a valid Graphics-API instance (typically thiat will simply be `this` in your code).
|
||||
- `getLUT(count)` returns an array of `count` coordinates of the form `{x:...,y:...}`, representing a polygonal approximation of this `BSpline`.
|
||||
- `formKnots(open = false)` set-and-return the list of B-spline knots using either the standard uniform interval spacing, or if `open` is set to `true`, special spacing to effect a B-spline that start and ends at the actual start and end coordinates. The knot array returned in this fashion is a live array, and updating its values **will** change the B-spline's shape.
|
||||
- `formUniformKnots()` set-and-return uniformaly spaced knot values. The knot array returned in this fashion is a live array, and updating its values **will** change the B-spline's shape.
|
||||
- `formWeights()` set-and-return the array of weights. These will all be uniformly initialized to 1, with the weight array returned being a live array, so that updating its values will change the B-spline's shape.
|
263
docs/js/graphics-element/api/base-api.js
Normal file
263
docs/js/graphics-element/api/base-api.js
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* The base API class, responsible for such things as setting up canvas event
|
||||
* handling, method accumulation, custom element binding, etc. etc.
|
||||
*
|
||||
* Basically if it's not part of the "public" API, it's in this class.
|
||||
*/
|
||||
class BaseAPI {
|
||||
static get privateMethods() {
|
||||
return [`constructor`, `createHatchPatterns`, `addListeners`, `getCursorCoords`].concat(this.superCallers).concat(this.eventHandlers);
|
||||
}
|
||||
|
||||
static get superCallers() {
|
||||
return [`setup`, `draw`];
|
||||
}
|
||||
|
||||
static get eventHandlers() {
|
||||
return [`onMouseDown`, `onMouseMove`, `onMouseUp`, `onKeyDown`, `onKeyUp`];
|
||||
}
|
||||
|
||||
static get methods() {
|
||||
const priv = this.privateMethods;
|
||||
const names = Object.getOwnPropertyNames(this.prototype).concat([`setSize`, `redraw`]);
|
||||
return names.filter((v) => priv.indexOf(v) < 0);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(
|
||||
uid,
|
||||
width = 200,
|
||||
height = 200,
|
||||
canvasBuildFunction, // Only used during image generation, not used in the browser
|
||||
customDataSet // " "
|
||||
) {
|
||||
if (uid) {
|
||||
this.element = window[uid];
|
||||
delete window[uid];
|
||||
this.dataset = this.element.dataset;
|
||||
}
|
||||
if (canvasBuildFunction) {
|
||||
const { canvas, ctx } = canvasBuildFunction(width, height);
|
||||
this.canvas = canvas;
|
||||
this.ctx = ctx;
|
||||
this.preSized = true;
|
||||
} else {
|
||||
this.canvas = document.createElement(`canvas`);
|
||||
}
|
||||
if (!this.dataset) {
|
||||
if (customDataSet) {
|
||||
this.dataset = customDataSet;
|
||||
} else {
|
||||
this.dataset = {};
|
||||
}
|
||||
}
|
||||
Object.defineProperty(this, `parameters`, {
|
||||
writable: false,
|
||||
configurable: false,
|
||||
value: Object.fromEntries(
|
||||
Object.entries(this.dataset)
|
||||
.map((pair) => {
|
||||
let name = pair[0];
|
||||
let v = pair[1];
|
||||
if (v === `null` || v === `undefined`) return [];
|
||||
if (v === `true`) return [name, true];
|
||||
else if (v === `false`) return [name, false];
|
||||
else {
|
||||
let d = parseFloat(v);
|
||||
// Use == to evaluate "is this a string number"
|
||||
if (v == d) return [name, d];
|
||||
}
|
||||
return [name, v];
|
||||
})
|
||||
.filter((v) => v.length)
|
||||
),
|
||||
});
|
||||
this.addListeners();
|
||||
this.setSize(width, height);
|
||||
this.currentPoint = false;
|
||||
this.frame = 0;
|
||||
this.setup();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
addListeners() {
|
||||
const canvas = this.canvas;
|
||||
|
||||
if (this.element) {
|
||||
this.element.setGraphic(this);
|
||||
}
|
||||
|
||||
this.cursor = {};
|
||||
|
||||
const root = typeof document !== "undefined" ? document : canvas;
|
||||
|
||||
[`touchstart`, `mousedown`].forEach((evtName) => canvas.addEventListener(evtName, (evt) => this.onMouseDown(evt)));
|
||||
|
||||
[`touchmove`, `mousemove`].forEach((evtName) =>
|
||||
canvas.addEventListener(evtName, (evt) => {
|
||||
this.onMouseMove(evt);
|
||||
// Force a redraw only if there are movable points,
|
||||
// and there is a current point bound, but only if
|
||||
// the subclass didn't already call redraw() as part
|
||||
// of its own mouseMove handling.
|
||||
if (this.movable.length && this.currentPoint && !this.redrawing) {
|
||||
this.redraw();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
[`touchend`, `mouseup`].forEach((evtName) => root.addEventListener(evtName, (evt) => this.onMouseUp(evt)));
|
||||
|
||||
this.keyboard = {};
|
||||
|
||||
[`keydown`].forEach((evtName) => canvas.addEventListener(evtName, (evt) => this.onKeyDown(evt)));
|
||||
|
||||
[`keyup`].forEach((evtName) => canvas.addEventListener(evtName, (evt) => this.onKeyUp(evt)));
|
||||
}
|
||||
|
||||
stopEvent(evt) {
|
||||
if (evt.target === this.canvas) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
getCursorCoords(evt) {
|
||||
const left = evt.target.offsetLeft,
|
||||
top = evt.target.offsetTop;
|
||||
|
||||
let x, y;
|
||||
|
||||
if (evt.targetTouches) {
|
||||
const touch = evt.targetTouches;
|
||||
for (let i = 0; i < touch.length; i++) {
|
||||
if (!touch[i] || !touch[i].pageX) continue;
|
||||
x = touch[i].pageX - left;
|
||||
y = touch[i].pageY - top;
|
||||
}
|
||||
} else {
|
||||
x = evt.pageX - left;
|
||||
y = evt.pageY - top;
|
||||
}
|
||||
this.cursor.x = x;
|
||||
this.cursor.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onMouseDown(evt) {
|
||||
this.stopEvent(evt);
|
||||
this.cursor.down = true;
|
||||
this.getCursorCoords(evt);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onMouseMove(evt) {
|
||||
this.stopEvent(evt);
|
||||
this.cursor.move = true;
|
||||
this.getCursorCoords(evt);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onMouseUp(evt) {
|
||||
this.stopEvent(evt);
|
||||
this.cursor.down = false;
|
||||
this.cursor.move = false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
safelyInterceptKey(evt) {
|
||||
// We don't want to interfere with the browser, so we're only
|
||||
// going to allow unmodified keys, or shift-modified keys,
|
||||
// and tab has to always work. For obvious reasons.
|
||||
const tab = evt.key !== "Tab";
|
||||
const functionKey = evt.key.match(/F\d+/) === null;
|
||||
const specificCheck = tab && functionKey;
|
||||
if (!evt.altKey && !evt.ctrlKey && !evt.metaKey && specificCheck) {
|
||||
this.stopEvent(evt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onKeyDown(evt) {
|
||||
this.safelyInterceptKey(evt);
|
||||
// FIXME: Known bug: this approach means that "shift + r + !shift + !r" leaves "R" set to true
|
||||
this.keyboard[evt.key] = true;
|
||||
this.keyboard.currentKey = evt.key;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onKeyUp(evt) {
|
||||
this.safelyInterceptKey(evt);
|
||||
// FIXME: Known bug: this approach means that "shift + r + !shift + !r" leaves "R" set to true
|
||||
this.keyboard[evt.key] = false;
|
||||
this.keyboard.currentKey = evt.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is critical in correctly sizing the canvas.
|
||||
*/
|
||||
setSize(w, h) {
|
||||
this.width = w || this.width;
|
||||
this.height = h || this.height;
|
||||
if (!this.preSized) {
|
||||
this.canvas.width = this.width;
|
||||
this.canvas.style.width = `${this.width}px`;
|
||||
this.canvas.height = this.height;
|
||||
this.ctx = this.canvas.getContext(`2d`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main entry point.
|
||||
*/
|
||||
setup() {
|
||||
// console.log(`setup`);
|
||||
this.movable = [];
|
||||
this.font = {
|
||||
size: 10,
|
||||
weight: 400,
|
||||
family: `arial`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the draw (loop) function.
|
||||
*/
|
||||
draw() {
|
||||
this.frame++;
|
||||
// console.log(`draw`);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is mostly a safety function, to
|
||||
* prevent direct calls to draw().. it might
|
||||
* disappear.
|
||||
*/
|
||||
redraw() {
|
||||
this.redrawing = true;
|
||||
this.draw();
|
||||
this.redrawing = false;
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseAPI };
|
816
docs/js/graphics-element/api/graphics-api.js
Normal file
816
docs/js/graphics-element/api/graphics-api.js
Normal file
@@ -0,0 +1,816 @@
|
||||
import { enrich } from "../lib/enrich.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";
|
||||
import impartSliderLogic from "./impart-slider-logic.js";
|
||||
|
||||
const MOUSE_PRECISION_ZONE = 5;
|
||||
const TOUCH_PRECISION_ZONE = 30;
|
||||
|
||||
let CURRENT_HUE = 0;
|
||||
let CURRENT_CURSOR = `pointer`;
|
||||
|
||||
/**
|
||||
* 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 CROSS() {
|
||||
return `crosshair`;
|
||||
}
|
||||
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`;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (this.canvas.style.cursor !== `none`) {
|
||||
this.setCursor(this.HAND);
|
||||
}
|
||||
return; // NOTE: this is a return, not a break!
|
||||
}
|
||||
}
|
||||
if (this.canvas.style.cursor !== `none`) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: universal free transform based on applying
|
||||
*
|
||||
* | a b c |
|
||||
* m = | d e f |
|
||||
* | 0 0 1 |
|
||||
*/
|
||||
transform(a, b, c, d, e, f) {
|
||||
this.ctx.transform(a, b, c, d, e, f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 a (CSS) margin around the canvas
|
||||
*/
|
||||
setMargin(width = 0) {
|
||||
this.canvas.style.marginTop = `${width}px`;
|
||||
this.canvas.style.marginBottom = `${width}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the cursor in a way that we can restore later.
|
||||
*/
|
||||
hideCursor() {
|
||||
this.canvas.style.cursor = `none`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebind the cursor to what it should be.
|
||||
*/
|
||||
showCursor() {
|
||||
this.canvas.style.cursor = CURRENT_CURSOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cursor type while the cursor is over the canvas
|
||||
*/
|
||||
setCursor(type) {
|
||||
CURRENT_CURSOR = type;
|
||||
this.showCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = 1) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current state/properties on a stack
|
||||
*/
|
||||
save() {
|
||||
this.ctx.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the most recently saved state/properties from the stack
|
||||
*/
|
||||
restore() {
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.save();
|
||||
this.resetTransform();
|
||||
this.ctx.fillStyle = color;
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.restore();
|
||||
if (!preserveTransforms) this.resetTransform();
|
||||
if (this._gridParams) {
|
||||
this.save();
|
||||
this.setStroke(this._gridParams.color);
|
||||
this.translate(0.5, 0.5);
|
||||
this.drawGrid(this._gridParams.size);
|
||||
this.translate(-0.5, -0.5);
|
||||
this.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, wedge = false) {
|
||||
this.ctx.beginPath();
|
||||
if (wedge) this.ctx.moveTo(x, y);
|
||||
this.ctx.arc(x, y, r, s, e);
|
||||
if (wedge) this.ctx.moveTo(x, y);
|
||||
this.ctx.fill();
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a circular wedge
|
||||
*/
|
||||
wedge(x, y, r, s, e) {
|
||||
this.arc(x, y, r, s, e, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw text on the canvas
|
||||
*/
|
||||
text(str, x, y, alignment) {
|
||||
if (y === undefined) {
|
||||
y = x.y;
|
||||
x = x.x;
|
||||
}
|
||||
const ctx = this.ctx;
|
||||
ctx.save();
|
||||
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.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(generator, 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 = generator(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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Yield a snapshot of the current shape.
|
||||
*/
|
||||
saveShape() {
|
||||
return this.currentShape;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(a, b) {
|
||||
if (a === undefined) {
|
||||
a = 0;
|
||||
b = 1;
|
||||
} else if (b === undefined) {
|
||||
b = a;
|
||||
a = 0;
|
||||
}
|
||||
return a + Math.random() * (b - a);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple inheritance is probably a good thing to not have in JS,
|
||||
* but traits would have been nice, because a 1000+ LoC class is kind
|
||||
* of silly, so to keep things easy to maintain, you end up writing
|
||||
* custom trait-adding functions like this...
|
||||
*/
|
||||
impartSliderLogic(GraphicsAPI);
|
||||
|
||||
export { GraphicsAPI, Bezier, BSpline, Vector, Matrix, Shape };
|
151
docs/js/graphics-element/api/impart-slider-logic.js
Normal file
151
docs/js/graphics-element/api/impart-slider-logic.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { create } from "../lib/create.js";
|
||||
|
||||
export default function impartSliderLogic(GraphicsAPI) {
|
||||
/**
|
||||
* Dynamically add a slider
|
||||
*/
|
||||
GraphicsAPI.prototype.addSlider = function 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);
|
||||
return 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
|
||||
*/
|
||||
GraphicsAPI.prototype.updateSlider = function 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.
|
||||
*/
|
||||
GraphicsAPI.prototype.setSlider = function 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;
|
||||
|
||||
// create a slider row in the table of sliders
|
||||
let ui = (() => {
|
||||
if (!this.element) {
|
||||
return { update: (v) => {} };
|
||||
}
|
||||
|
||||
let table = find(`table.slider-wrapper`);
|
||||
|
||||
if (!table) {
|
||||
table = slider.parentNode.querySelector(`table.slider-wrapper`);
|
||||
if (!table) {
|
||||
table = create(`table`);
|
||||
table.classList.add(`slider-wrapper`);
|
||||
slider.parentNode.replaceChild(table, slider);
|
||||
}
|
||||
}
|
||||
|
||||
let tr = create(`tr`);
|
||||
|
||||
let td = create(`td`);
|
||||
let label = create(`label`);
|
||||
label.classList.add(`slider-label`);
|
||||
label.innerHTML = propLabel;
|
||||
td.append(label);
|
||||
tr.append(td);
|
||||
|
||||
td = create(`td`);
|
||||
slider.classList.add(`slider`);
|
||||
this._sliders[propname] = slider;
|
||||
td.append(slider);
|
||||
tr.append(td);
|
||||
|
||||
td = create(`td`);
|
||||
let valueField = create(`label`);
|
||||
valueField.classList.add(`slider-value`);
|
||||
valueField.textContent;
|
||||
td.append(valueField);
|
||||
tr.append(td);
|
||||
|
||||
table.append(tr);
|
||||
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;
|
||||
}
|
||||
|
||||
let step = slider.getAttribute(`step`) || "1";
|
||||
let res = !step.includes(`.`) ? 0 : step.substring(step.indexOf(`.`) + 1).length;
|
||||
|
||||
slider.updateProperty = (evt) => {
|
||||
let value = parseFloat(slider.value);
|
||||
ui.update(value.toFixed(res));
|
||||
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.toFixed(res));
|
||||
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
|
||||
*/
|
||||
GraphicsAPI.prototype.removeSliders = function removeSliders() {
|
||||
this.findAll(`.slider-wrapper`).forEach((s) => {
|
||||
s.parentNode.removeChild(s);
|
||||
});
|
||||
};
|
||||
|
||||
return GraphicsAPI;
|
||||
}
|
178
docs/js/graphics-element/api/types/bezier.js
Normal file
178
docs/js/graphics-element/api/types/bezier.js
Normal file
@@ -0,0 +1,178 @@
|
||||
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
|
||||
*/
|
||||
class Bezier extends Original {
|
||||
static defaultQuadratic(apiInstance) {
|
||||
if (!apiInstance) {
|
||||
throw new Error(`missing reference of API instance in Bezier.defaultQuadratic(instance)`);
|
||||
}
|
||||
return new Bezier(apiInstance, 70, 250, 20, 110, 220, 60);
|
||||
}
|
||||
|
||||
static defaultCubic(apiInstance) {
|
||||
if (!apiInstance) {
|
||||
throw new Error(`missing reference of API instance in Bezier.defaultCubic(instance)`);
|
||||
}
|
||||
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(`missing reference of API instance in Bezier constructor`);
|
||||
}
|
||||
super(...coords);
|
||||
this.api = apiInstance;
|
||||
this.ctx = apiInstance.ctx;
|
||||
}
|
||||
|
||||
project(x, y) {
|
||||
return super.project({ x, y });
|
||||
}
|
||||
|
||||
getPointNear(x, y, d = 5) {
|
||||
const p = this.points;
|
||||
for (let i = 0, e = p.length; i < e; i++) {
|
||||
let dx = Math.abs(p[i].x - x);
|
||||
let dy = Math.abs(p[i].y - y);
|
||||
if ((dx * dx + dy * dy) ** 0.5 <= d) {
|
||||
return p[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawCurve(color = `#333`) {
|
||||
const ctx = this.ctx;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.beginPath();
|
||||
const lut = this.getLUT().slice();
|
||||
let p = lut.shift();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
while (lut.length) {
|
||||
p = lut.shift();
|
||||
ctx.lineTo(p.x, p.y);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawPoints(labels = true) {
|
||||
const colors = [`red`, `green`, `blue`, `yellow`, `orange`, `cyan`, `magenta`];
|
||||
const api = this.api;
|
||||
const ctx = this.ctx;
|
||||
|
||||
ctx.save();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = `#999`;
|
||||
this.points.forEach((p, i) => {
|
||||
api.setFill(colors[i % colors.length]);
|
||||
api.circle(p.x, p.y, 5);
|
||||
if (labels) {
|
||||
api.setFill(`black`);
|
||||
let x = p.x | 0;
|
||||
let y = p.y | 0;
|
||||
api.text(`(${x},${y})`, x + 10, y + 10);
|
||||
}
|
||||
});
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawSkeleton(color = `#555`) {
|
||||
const api = this.api;
|
||||
const ctx = this.ctx;
|
||||
ctx.save();
|
||||
const p = this.points;
|
||||
api.noFill();
|
||||
api.setStroke(color);
|
||||
api.start();
|
||||
p.forEach((v) => api.vertex(v.x, v.y));
|
||||
api.end();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
getStrutPoints(t) {
|
||||
const p = this.points.map((p) => new Vector(p));
|
||||
const mt = 1 - t;
|
||||
|
||||
let s = 0;
|
||||
let n = p.length + 1;
|
||||
while (--n > 1) {
|
||||
let list = p.slice(s, s + n);
|
||||
for (let i = 0, e = list.length - 1; i < e; i++) {
|
||||
let pt = list[i + 1].subtract(list[i + 1].subtract(list[i]).scale(mt));
|
||||
p.push(pt);
|
||||
}
|
||||
s += n;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
drawStruts(t, color = `black`, showpoints = true) {
|
||||
const p = t.forEach ? t : this.getStrutPoints(t);
|
||||
|
||||
const api = this.api;
|
||||
const ctx = api.ctx;
|
||||
ctx.save();
|
||||
api.noFill();
|
||||
api.setStroke(color);
|
||||
|
||||
let s = this.points.length;
|
||||
let n = this.points.length;
|
||||
while (--n > 1) {
|
||||
api.start();
|
||||
for (let i = 0; i < n; i++) {
|
||||
let pt = p[s + i];
|
||||
api.vertex(pt.x, pt.y);
|
||||
if (showpoints) api.circle(pt.x, pt.y, 5);
|
||||
}
|
||||
api.end();
|
||||
s += n;
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
drawBoundingBox(color = `black`) {
|
||||
let bbox = this.bbox(),
|
||||
mx = bbox.x.min,
|
||||
my = bbox.y.min,
|
||||
MX = bbox.x.max,
|
||||
MY = bbox.y.max,
|
||||
api = this.api;
|
||||
api.save();
|
||||
api.noFill();
|
||||
api.setStroke(color);
|
||||
api.rect(mx, my, MX - mx, MY - my);
|
||||
api.restore();
|
||||
}
|
||||
}
|
||||
|
||||
export { Bezier };
|
58
docs/js/graphics-element/api/types/bspline.js
Normal file
58
docs/js/graphics-element/api/types/bspline.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import interpolate from "../util/interpolate-bspline.js";
|
||||
|
||||
// cubic B-Spline
|
||||
const DEGREE = 3;
|
||||
|
||||
class BSpline {
|
||||
constructor(apiInstance, points) {
|
||||
this.api = apiInstance;
|
||||
this.ctx = apiInstance.ctx;
|
||||
|
||||
// the spline library needs points in array format [x,y] rather than object format {x:..., y:...}
|
||||
this.points = points.map((v) => {
|
||||
if (v instanceof Array) return v;
|
||||
return [v.x, v.y];
|
||||
});
|
||||
}
|
||||
|
||||
getLUT(count) {
|
||||
let c = count - 1;
|
||||
return [...new Array(count)].map((_, i) => {
|
||||
let p = interpolate(i / c, DEGREE, this.points, this.knots, this.weights);
|
||||
return { x: p[0], y: p[1] };
|
||||
});
|
||||
}
|
||||
|
||||
formKnots(open = false) {
|
||||
if (!open) return this.formUniformKnots();
|
||||
|
||||
let knots = [],
|
||||
l = this.points.length,
|
||||
m = l - DEGREE;
|
||||
|
||||
// form the open-uniform knot vector
|
||||
for (let i = 1; i < l - DEGREE; i++) {
|
||||
knots.push(i + DEGREE);
|
||||
}
|
||||
// add [degree] zeroes at the front
|
||||
for (let i = 0; i <= DEGREE; i++) {
|
||||
knots = [DEGREE].concat(knots);
|
||||
}
|
||||
// add [degree] max-values to the back
|
||||
for (let i = 0; i <= DEGREE; i++) {
|
||||
knots.push(m + DEGREE);
|
||||
}
|
||||
|
||||
return (this.knots = knots);
|
||||
}
|
||||
|
||||
formUniformKnots() {
|
||||
return (this.knots = [...new Array(this.points.length + DEGREE + 1)].map((_, i) => i));
|
||||
}
|
||||
|
||||
formWeights() {
|
||||
return (this.weights = this.points.map((p) => 1));
|
||||
}
|
||||
}
|
||||
|
||||
export { BSpline };
|
165
docs/js/graphics-element/api/types/matrix.js
Normal file
165
docs/js/graphics-element/api/types/matrix.js
Normal file
@@ -0,0 +1,165 @@
|
||||
function invert(M) {
|
||||
// Copied from http://blog.acipo.com/matrix-inversion-in-javascript/
|
||||
// With permission, http://blog.acipo.com/matrix-inversion-in-javascript/#comment-5057289889
|
||||
|
||||
// (1) 'augment' the matrix (left) by the identity (on the right)
|
||||
// (2) Turn the matrix on the left into the identity by elemetry row ops
|
||||
// (3) The matrix on the right is the inverse (was the identity matrix)
|
||||
// There are 3 elemtary row ops:
|
||||
// (a) Swap 2 rows
|
||||
// (b) Multiply a row by a scalar
|
||||
// (c) Add 2 rows
|
||||
|
||||
//if the matrix isn't square: exit (error)
|
||||
if (M.length !== M[0].length) {
|
||||
console.log("not square");
|
||||
return;
|
||||
}
|
||||
|
||||
//create the identity matrix (I), and a copy (C) of the original
|
||||
var i = 0,
|
||||
ii = 0,
|
||||
j = 0,
|
||||
dim = M.length,
|
||||
e = 0,
|
||||
t = 0;
|
||||
var I = [],
|
||||
C = [];
|
||||
for (i = 0; i < dim; i += 1) {
|
||||
// Create the row
|
||||
I[I.length] = [];
|
||||
C[C.length] = [];
|
||||
for (j = 0; j < dim; j += 1) {
|
||||
//if we're on the diagonal, put a 1 (for identity)
|
||||
if (i == j) {
|
||||
I[i][j] = 1;
|
||||
} else {
|
||||
I[i][j] = 0;
|
||||
}
|
||||
|
||||
// Also, make the copy of the original
|
||||
C[i][j] = M[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
// Perform elementary row operations
|
||||
for (i = 0; i < dim; i += 1) {
|
||||
// get the element e on the diagonal
|
||||
e = C[i][i];
|
||||
|
||||
// if we have a 0 on the diagonal (we'll need to swap with a lower row)
|
||||
if (e == 0) {
|
||||
//look through every row below the i'th row
|
||||
for (ii = i + 1; ii < dim; ii += 1) {
|
||||
//if the ii'th row has a non-0 in the i'th col
|
||||
if (C[ii][i] != 0) {
|
||||
//it would make the diagonal have a non-0 so swap it
|
||||
for (j = 0; j < dim; j++) {
|
||||
e = C[i][j]; //temp store i'th row
|
||||
C[i][j] = C[ii][j]; //replace i'th row by ii'th
|
||||
C[ii][j] = e; //repace ii'th by temp
|
||||
e = I[i][j]; //temp store i'th row
|
||||
I[i][j] = I[ii][j]; //replace i'th row by ii'th
|
||||
I[ii][j] = e; //repace ii'th by temp
|
||||
}
|
||||
//don't bother checking other rows since we've swapped
|
||||
break;
|
||||
}
|
||||
}
|
||||
//get the new diagonal
|
||||
e = C[i][i];
|
||||
//if it's still 0, not invertable (error)
|
||||
if (e == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Scale this row down by e (so we have a 1 on the diagonal)
|
||||
for (j = 0; j < dim; j++) {
|
||||
C[i][j] = C[i][j] / e; //apply to original matrix
|
||||
I[i][j] = I[i][j] / e; //apply to identity
|
||||
}
|
||||
|
||||
// Subtract this row (scaled appropriately for each row) from ALL of
|
||||
// the other rows so that there will be 0's in this column in the
|
||||
// rows above and below this one
|
||||
for (ii = 0; ii < dim; ii++) {
|
||||
// Only apply to other rows (we want a 1 on the diagonal)
|
||||
if (ii == i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to change this element to 0
|
||||
e = C[ii][i];
|
||||
|
||||
// Subtract (the row above(or below) scaled by e) from (the
|
||||
// current row) but start at the i'th column and assume all the
|
||||
// stuff left of diagonal is 0 (which it should be if we made this
|
||||
// algorithm correctly)
|
||||
for (j = 0; j < dim; j++) {
|
||||
C[ii][j] -= e * C[i][j]; //apply to original matrix
|
||||
I[ii][j] -= e * I[i][j]; //apply to identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//we've done all operations, C should be the identity
|
||||
//matrix I should be the inverse:
|
||||
return I;
|
||||
}
|
||||
|
||||
function multiply(m1, m2) {
|
||||
var M = [];
|
||||
var m2t = transpose(m2);
|
||||
m1.forEach((row, r) => {
|
||||
M[r] = [];
|
||||
m2t.forEach((col, c) => {
|
||||
M[r][c] = row.map((v, i) => col[i] * v).reduce((a, v) => a + v, 0);
|
||||
});
|
||||
});
|
||||
return M;
|
||||
}
|
||||
|
||||
function transpose(M) {
|
||||
return M[0].map((col, i) => M.map((row) => row[i]));
|
||||
}
|
||||
|
||||
class Matrix {
|
||||
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(j) {
|
||||
var d = this.data,
|
||||
col = [];
|
||||
for (let r = 0, l = d.length; r < l; r++) {
|
||||
col.push(d[r][j]);
|
||||
}
|
||||
return col;
|
||||
}
|
||||
multiply(other) {
|
||||
return new Matrix(multiply(this.data, other.data));
|
||||
}
|
||||
invert() {
|
||||
return new Matrix(invert(this.data));
|
||||
}
|
||||
transpose() {
|
||||
return new Matrix(transpose(this.data));
|
||||
}
|
||||
}
|
||||
|
||||
export { Matrix };
|
73
docs/js/graphics-element/api/types/vector.js
Normal file
73
docs/js/graphics-element/api/types/vector.js
Normal file
@@ -0,0 +1,73 @@
|
||||
class Vector {
|
||||
constructor(x, y, z) {
|
||||
if (arguments.length === 1) {
|
||||
z = x.z;
|
||||
y = x.y;
|
||||
x = x.x;
|
||||
}
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
if (z !== undefined) {
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
dist(other, y, z = 0) {
|
||||
if (y !== undefined) other = { x: other, y, z };
|
||||
let sum = 0;
|
||||
sum += (this.x - other.x) ** 2;
|
||||
sum += (this.y - other.y) ** 2;
|
||||
let z1 = this.z ? this.z : 0;
|
||||
let z2 = other.z ? other.z : 0;
|
||||
sum += (z1 - z2) ** 2;
|
||||
return sum ** 0.5;
|
||||
}
|
||||
normalize(f) {
|
||||
let mag = this.dist(0, 0, 0);
|
||||
return new Vector((f * this.x) / mag, (f * this.y) / mag, (f * this.z) / mag);
|
||||
}
|
||||
getAngle() {
|
||||
return -Math.atan2(this.y, this.x);
|
||||
}
|
||||
reflect(other) {
|
||||
let p = new Vector(other.x - this.x, other.y - this.y);
|
||||
if (other.z !== undefined) {
|
||||
p.z = other.z;
|
||||
if (this.z !== undefined) {
|
||||
p.z -= this.z;
|
||||
}
|
||||
}
|
||||
return this.subtract(p);
|
||||
}
|
||||
add(other) {
|
||||
let p = new Vector(this.x + other.x, this.y + other.y);
|
||||
if (this.z !== undefined) {
|
||||
p.z = this.z;
|
||||
if (other.z !== undefined) {
|
||||
p.z += other.z;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
subtract(other) {
|
||||
let p = new Vector(this.x - other.x, this.y - other.y);
|
||||
if (this.z !== undefined) {
|
||||
p.z = this.z;
|
||||
if (other.z !== undefined) {
|
||||
p.z -= other.z;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
scale(f = 1) {
|
||||
if (f === 0) {
|
||||
return new Vector(0, 0, this.z === undefined ? undefined : 0);
|
||||
}
|
||||
let p = new Vector(this.x * f, this.y * f);
|
||||
if (this.z !== undefined) {
|
||||
p.z = this.z * f;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
export { Vector };
|
21
docs/js/graphics-element/api/util/binomial.js
Normal file
21
docs/js/graphics-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/graphics-element/api/util/fit-curve-to-points.js
Normal file
86
docs/js/graphics-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 };
|
88
docs/js/graphics-element/api/util/interpolate-bspline.js
Normal file
88
docs/js/graphics-element/api/util/interpolate-bspline.js
Normal file
@@ -0,0 +1,88 @@
|
||||
// https://github.com/thibauts/b-spline
|
||||
export default function interpolate(t, degree, points, knots, weights, result, scaled) {
|
||||
var i, j, s, l; // function-scoped iteration variables
|
||||
var n = points.length; // points count
|
||||
var d = points[0].length; // point dimensionality
|
||||
|
||||
if (degree < 1) throw new Error("degree must be at least 1 (linear)");
|
||||
if (degree > n - 1) throw new Error("degree must be less than or equal to point count - 1");
|
||||
|
||||
if (!weights) {
|
||||
// build weight vector of length [n]
|
||||
weights = [];
|
||||
for (i = 0; i < n; i++) {
|
||||
weights[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// closed curve?
|
||||
if (weights.length < points.length) {
|
||||
weights = weights.concat(weights.slice(0, degree));
|
||||
}
|
||||
|
||||
if (!knots) {
|
||||
// build knot vector of length [n + degree + 1]
|
||||
var knots = [];
|
||||
for (i = 0; i < n + degree + 1; i++) {
|
||||
knots[i] = i;
|
||||
}
|
||||
} else {
|
||||
if (knots.length !== n + degree + 1) throw new Error("bad knot vector length");
|
||||
}
|
||||
|
||||
// closed curve?
|
||||
if (knots.length === points.length) {
|
||||
knots = knots.concat(knots.slice(0, degree));
|
||||
}
|
||||
|
||||
var domain = [degree, knots.length - 1 - degree];
|
||||
|
||||
var low = knots[domain[0]];
|
||||
var high = knots[domain[1]];
|
||||
|
||||
// remap t to the domain where the spline is defined
|
||||
if (!scaled) {
|
||||
t = t * (high - low) + low;
|
||||
}
|
||||
|
||||
if (t < low || t > high) throw new Error("out of bounds");
|
||||
|
||||
// find s (the spline segment) for the [t] value provided
|
||||
for (s = domain[0]; s < domain[1]; s++) {
|
||||
if (t >= knots[s] && t <= knots[s + 1]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// convert points to homogeneous coordinates
|
||||
var v = [];
|
||||
for (i = 0; i < n; i++) {
|
||||
v[i] = [];
|
||||
for (j = 0; j < d; j++) {
|
||||
v[i][j] = points[i][j] * weights[i];
|
||||
}
|
||||
v[i][d] = weights[i];
|
||||
}
|
||||
|
||||
// l (level) goes from 1 to the curve degree + 1
|
||||
var alpha;
|
||||
for (l = 1; l <= degree + 1; l++) {
|
||||
// build level l of the pyramid
|
||||
for (i = s; i > s - degree - 1 + l; i--) {
|
||||
alpha = (t - knots[i]) / (knots[i + degree + 1 - l] - knots[i]);
|
||||
|
||||
// interpolate each component
|
||||
for (j = 0; j < d + 1; j++) {
|
||||
v[i][j] = (1 - alpha) * v[i - 1][j] + alpha * v[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert back to cartesian and return
|
||||
var result = result || [];
|
||||
for (i = 0; i < d; i++) {
|
||||
result[i] = v[s][i] / v[s][d];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
61
docs/js/graphics-element/api/util/shape.js
Normal file
61
docs/js/graphics-element/api/util/shape.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* A complex shape, represented as a collection of paths
|
||||
* that can be either polygon, Catmull-Rom curves, or
|
||||
* cubic Bezier curves.
|
||||
*/
|
||||
class Shape {
|
||||
constructor(type, factor, points = []) {
|
||||
this.first = false;
|
||||
this.segments = [];
|
||||
this.newSegment(type, factor);
|
||||
points.forEach((p) => this.vertex(p));
|
||||
}
|
||||
merge(other) {
|
||||
if (!other.segments) {
|
||||
other = { segments: [new Segment(Shape.POLYGON, undefined, other)] };
|
||||
}
|
||||
other.segments.forEach((s) => this.segments.push(s));
|
||||
}
|
||||
copy() {
|
||||
const copy = new Shape(this.type, this.factor);
|
||||
copy.first = this.first;
|
||||
copy.segments = this.segments.map((s) => s.copy());
|
||||
return copy;
|
||||
}
|
||||
newSegment(type, factor) {
|
||||
this.currentSegment = new Segment(type, factor);
|
||||
this.segments.push(this.currentSegment);
|
||||
}
|
||||
vertex(p) {
|
||||
if (!this.first) this.first = p;
|
||||
else this.currentSegment.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pathing type constants
|
||||
*/
|
||||
Shape.POLYGON = `Polygon`;
|
||||
Shape.CURVE = `CatmullRom`;
|
||||
Shape.BEZIER = `Bezier`;
|
||||
|
||||
/**
|
||||
* A shape subpath
|
||||
*/
|
||||
class Segment {
|
||||
constructor(type, factor, points = []) {
|
||||
this.type = type;
|
||||
this.factor = factor;
|
||||
this.points = points;
|
||||
}
|
||||
copy() {
|
||||
const copy = new Segment(this.type, this.factor);
|
||||
copy.points = JSON.parse(JSON.stringify(this.points));
|
||||
return copy;
|
||||
}
|
||||
add(p) {
|
||||
this.points.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
export { Shape, Segment };
|
Reference in New Issue
Block a user