mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-26 09:44:32 +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 };
|
120
docs/js/graphics-element/custom-element.js
Normal file
120
docs/js/graphics-element/custom-element.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const REG_KEY = `registered as custom element`;
|
||||
|
||||
// helper function
|
||||
function NotImplemented(instance, fname) {
|
||||
console.warn(`missing implementation for ${fname}(...data) in ${instance.__proto__.constructor.name}`);
|
||||
}
|
||||
|
||||
// helper function for turning "ClassName" into "class-name"
|
||||
function getElementTagName(cls) {
|
||||
return cls.prototype.constructor.name.replace(/([A-Z])([a-z])/g, (a, b, c, d) => {
|
||||
const r = `${b.toLowerCase()}${c}`;
|
||||
return d > 0 ? `-${r}` : r;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an enrichment class to make working with custom elements
|
||||
* actually pleasant, rather than a ridiculous exercise in figuring
|
||||
* out a low-level spec.
|
||||
*/
|
||||
class CustomElement extends HTMLElement {
|
||||
static register(cls) {
|
||||
if (!cls[REG_KEY]) {
|
||||
const tagName = cls.tagName || getElementTagName(cls);
|
||||
customElements.define(tagName, cls);
|
||||
cls[REG_KEY] = true;
|
||||
return customElements.whenDefined(tagName);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
static get tagName() {
|
||||
return getElementTagName(this);
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
if (!customElements.resolveScope) {
|
||||
customElements.resolveScope = function (scope) {
|
||||
try {
|
||||
return scope.getRootNode().host;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
return window;
|
||||
};
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
|
||||
const route = {
|
||||
childList: (record) => {
|
||||
this.handleChildChanges(Array.from(record.addedNodes), Array.from(record.removedNodes));
|
||||
this.render();
|
||||
},
|
||||
attributes: (record) => {
|
||||
this.handleAttributeChange(record.attributeName, record.oldValue, this.getAttribute(record.attributeName));
|
||||
this.render();
|
||||
},
|
||||
};
|
||||
|
||||
// Set up a mutation observer because there are no custom
|
||||
// element lifecycle functions for changes to the childNodes
|
||||
// nodelist.
|
||||
|
||||
this._observer = new MutationObserver((records) => {
|
||||
if (this.isConnected) {
|
||||
records.forEach((record) => {
|
||||
// console.log(record);
|
||||
route[record.type](record);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this._observer.observe(this, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
});
|
||||
|
||||
// Set up an open shadow DOM, because the web is open,
|
||||
// and hiding your internals is ridiculous.
|
||||
|
||||
const shadowProps = { mode: `open` };
|
||||
|
||||
this._shadow = this.attachShadow(shadowProps);
|
||||
this._style = document.createElement(`style`);
|
||||
this._style.textContent = this.getStyle();
|
||||
|
||||
if (this._options.header !== false) this._header = document.createElement(`header`);
|
||||
if (this._options.slot !== false && this._options.void !== true) this._slot = document.createElement(`slot`);
|
||||
if (this._options.footer !== false) this._footer = document.createElement(`footer`);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
handleChildChanges(added, removed) {
|
||||
if (!this._options.void) NotImplemented(this, `handleChildChanges`);
|
||||
}
|
||||
|
||||
handleAttributeChange(name, oldValue, newValue) {
|
||||
NotImplemented(this, `handleAttributeChange`);
|
||||
}
|
||||
|
||||
getStyle() {
|
||||
return ``;
|
||||
}
|
||||
|
||||
render() {
|
||||
this._shadow.innerHTML = ``;
|
||||
this._shadow.append(this._style);
|
||||
if (this._options.header !== false) this._shadow.append(this._header);
|
||||
if (this._options.slot !== false) this._shadow.append(this._slot);
|
||||
if (this._options.footer !== false) this._shadow.append(this._footer);
|
||||
}
|
||||
}
|
||||
|
||||
export { CustomElement };
|
89
docs/js/graphics-element/graphics-element.css
Normal file
89
docs/js/graphics-element/graphics-element.css
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Let's look at how we can give the <graphics-element> a decent look,
|
||||
but also how we can make sure that the <fallback> content only shows
|
||||
when the <graphics-element> isn't actually defined, because scripts
|
||||
are disabled (or blocked!).
|
||||
*/
|
||||
|
||||
graphics-element {
|
||||
display: inline-block;
|
||||
border: 1px solid grey;
|
||||
}
|
||||
|
||||
/*
|
||||
We can use the :defined pseudo-class to check whether a particular
|
||||
element is considered a "real" element (either by being a built-in
|
||||
standard HTML element, or a registered custom element) or whether it's
|
||||
just "a tag name that isn't tied to anything".
|
||||
|
||||
If JS is disabled, as well as when the JS for registering our custom
|
||||
element is still loading, our <graphics-element> will just be "a word"
|
||||
and so we want to make sure to not show any content, except for the
|
||||
<fallback-image>, if there is one.
|
||||
|
||||
So, first off: hide everything!
|
||||
*/
|
||||
|
||||
graphics-element:not(:defined) > * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
And then we declare a more specific rule that does NOT hide the
|
||||
<fallback-image> element and its content.
|
||||
*/
|
||||
|
||||
graphics-element:not(:defined) fallback-image {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 0.5em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Normally, images are inline elements, but in our case we want it to be
|
||||
treated as a full block, so that the caption text shows up underneath
|
||||
it, rather than next to it:
|
||||
*/
|
||||
graphics-element:not(:defined) fallback-image > .view-source {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -0.6em;
|
||||
margin-bottom: -0.2em;
|
||||
font-size: 60%;
|
||||
color: #00000030;
|
||||
}
|
||||
|
||||
graphics-element:not(:defined) fallback-image > label {
|
||||
display: block;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
graphics-element:not(:defined) fallback-image > img {
|
||||
display: block;
|
||||
border: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
/*
|
||||
Then, we say what should happen once our <graphics-element> element
|
||||
is considered a proper, registered HTML tag:
|
||||
*/
|
||||
|
||||
graphics-element:defined {
|
||||
display: inline-block;
|
||||
padding: 0.5em;
|
||||
justify-self: center;
|
||||
font-size: revert;
|
||||
text-align: revert;
|
||||
}
|
||||
|
||||
/*
|
||||
And of course, once that's the case we actually want to make sure that
|
||||
the <fallback-image> does NOT show anymore!
|
||||
*/
|
||||
|
||||
graphics-element:defined fallback-image.loaded {
|
||||
display: none;
|
||||
}
|
365
docs/js/graphics-element/graphics-element.js
Normal file
365
docs/js/graphics-element/graphics-element.js
Normal file
@@ -0,0 +1,365 @@
|
||||
import { CustomElement } from "./custom-element.js";
|
||||
|
||||
import splitCodeSections from "./lib/split-code-sections.js";
|
||||
import performCodeSurgery from "./lib/perform-code-surgery.js";
|
||||
|
||||
const MODULE_URL = import.meta.url;
|
||||
const MODULE_PATH = MODULE_URL.slice(0, MODULE_URL.lastIndexOf(`/`));
|
||||
|
||||
// Until global `await` gets added to JS, we need to declare this "constant"
|
||||
// using the `let` keyword instead, and then boostrap its value during the
|
||||
// `loadSource` call (using the standard if(undefined){assignvalue} pattern).
|
||||
let IMPORT_GLOBALS_FROM_GRAPHICS_API = undefined;
|
||||
|
||||
// Really wish this was baked into the DOM API. Having to use an
|
||||
// IntersectionObserver with bounding box fallback is super dumb
|
||||
// from an authoring perspective.
|
||||
function isInViewport(e) {
|
||||
if (typeof window === `undefined`) return true;
|
||||
if (typeof document === `undefined`) return true;
|
||||
|
||||
var b = e.getBoundingClientRect();
|
||||
return (
|
||||
b.top >= 0 &&
|
||||
b.left >= 0 &&
|
||||
b.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
b.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple "for programming code" element, for holding entire
|
||||
* programs, rather than code snippets.
|
||||
*/
|
||||
CustomElement.register(class ProgramCode extends HTMLElement {});
|
||||
|
||||
/**
|
||||
* Our custom element
|
||||
*/
|
||||
class GraphicsElement extends CustomElement {
|
||||
/**
|
||||
* Create an instance of this element
|
||||
*/
|
||||
constructor() {
|
||||
super({ header: false, footer: false });
|
||||
|
||||
this.originalHTML = this.outerHTML;
|
||||
|
||||
// Do we load immediately?
|
||||
if (isInViewport(this)) {
|
||||
this.loadSource();
|
||||
}
|
||||
|
||||
// Or do we load later, once we've been scrolled into view?
|
||||
else {
|
||||
let fallback = this.querySelector(`img`);
|
||||
new IntersectionObserver(
|
||||
(entries, observer) =>
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.loadSource();
|
||||
observer.disconnect();
|
||||
}
|
||||
}),
|
||||
{ threshold: 0.1, rootMargin: `${window.innerHeight}px` }
|
||||
).observe(fallback);
|
||||
}
|
||||
|
||||
this.label = document.createElement(`label`);
|
||||
if (!this.title) this.title = ``;
|
||||
this.label.textContent = this.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* part of the CustomElement API
|
||||
*/
|
||||
getStyle() {
|
||||
return `
|
||||
:host([hidden]) { display: none; }
|
||||
:host { max-width: calc(2em + ${this.getAttribute(`width`)}px); }
|
||||
:host style { display: none; }
|
||||
:host .top-title { display: flex; flex-direction: row-reverse; justify-content: space-between; }
|
||||
:host canvas { position: relative; z-index: 1; display: block; margin: auto; border-radius: 0; box-sizing: content-box!important; border: 1px solid lightgrey; }
|
||||
:host canvas:focus { border: 1px solid red; }
|
||||
:host a.view-source { font-size: 60%; text-decoration: none; }
|
||||
:host button.reset { font-size: 0.5em; }
|
||||
:host label { display: block; font-style:italic; font-size: 0.9em; text-align: right; }
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* part of the CustomElement API
|
||||
*/
|
||||
handleChildChanges(added, removed) {
|
||||
// debugLog(`child change:`, added, removed);
|
||||
}
|
||||
|
||||
/**
|
||||
* part of the CustomElement API
|
||||
*/
|
||||
handleAttributeChange(name, oldValue, newValue) {
|
||||
if (name === `title`) {
|
||||
this.label.textContent = this.getAttribute(`title`);
|
||||
}
|
||||
if (this.apiInstance) {
|
||||
let instance = this.apiInstance;
|
||||
if (name === `width`) {
|
||||
instance.setSize(parseInt(newValue), false);
|
||||
instance.redraw();
|
||||
}
|
||||
if (name === `height`) {
|
||||
instance.setSize(false, parseInt(newValue));
|
||||
instance.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the graphics code, either from a src URL, a <program-code> element, or .textContent
|
||||
*/
|
||||
async loadSource() {
|
||||
debugLog(`loading ${this.getAttribute(`src`)}`);
|
||||
|
||||
if (!IMPORT_GLOBALS_FROM_GRAPHICS_API) {
|
||||
const importStatement = (await fetch(`${MODULE_PATH}/api/graphics-api.js`).then((r) => r.text()))
|
||||
.match(/(export { [^}]+ })/)[0]
|
||||
.replace(`export`, `import`);
|
||||
IMPORT_GLOBALS_FROM_GRAPHICS_API = `${importStatement} from "${MODULE_PATH}/api/graphics-api.js"`;
|
||||
}
|
||||
|
||||
let src = false;
|
||||
let codeElement = this.querySelector(`program-code`);
|
||||
|
||||
let code = ``;
|
||||
|
||||
if (codeElement) {
|
||||
src = codeElement.getAttribute("src");
|
||||
if (src) {
|
||||
this.src = src;
|
||||
code = await fetch(src).then((response) => response.text());
|
||||
} else {
|
||||
code = codeElement.textContent;
|
||||
}
|
||||
} else {
|
||||
src = this.getAttribute("src");
|
||||
if (src) {
|
||||
this.src = src;
|
||||
code = await fetch(src).then((response) => response.text());
|
||||
} else {
|
||||
code = this.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
if (!codeElement) {
|
||||
codeElement = document.createElement(`program-code`);
|
||||
codeElement.textContent = code;
|
||||
this.prepend(codeElement);
|
||||
}
|
||||
|
||||
codeElement.setAttribute(`hidden`, `hidden`);
|
||||
|
||||
new MutationObserver((_records) => {
|
||||
// nornmally we don't want to completely recreate the shadow DOM
|
||||
this.processSource(src, codeElement.textContent);
|
||||
}).observe(codeElement, {
|
||||
characterData: true,
|
||||
attributes: false,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
// But on the first pass, we do.
|
||||
this.processSource(src, code, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the graphics source code into global and class code.
|
||||
*/
|
||||
processSource(src, code, rerender = false) {
|
||||
if (this.script) {
|
||||
if (this.script.parentNode) {
|
||||
this.script.parentNode.removeChild(this.script);
|
||||
}
|
||||
this.canvas.parentNode.removeChild(this.canvas);
|
||||
rerender = true;
|
||||
}
|
||||
|
||||
const uid = (this.uid = `bg-uid-${Date.now()}-${`${Math.random()}`.replace(`0.`, ``)}`);
|
||||
window[uid] = this;
|
||||
|
||||
// Step 1: fix the imports. This is ... a bit of work.
|
||||
let path;
|
||||
let base = document.querySelector(`base`);
|
||||
if (base) {
|
||||
path = base.href;
|
||||
} else {
|
||||
let loc = window.location.toString();
|
||||
path = loc.substring(0, loc.lastIndexOf(`/`) + 1);
|
||||
}
|
||||
let modulepath = `${path}${src}`;
|
||||
let modulebase = modulepath.substring(0, modulepath.lastIndexOf(`/`) + 1);
|
||||
|
||||
// okay, I lied, it's actually quite a lot of work.
|
||||
code = code.replace(/(import .+? from) "([^"]+)"/g, (_, main, group) => {
|
||||
return `${main} "${modulebase}${group}"`;
|
||||
});
|
||||
|
||||
this.linkableCode = code;
|
||||
|
||||
// Then, step 2: split up the code into "global" vs. "class" code.
|
||||
const split = splitCodeSections(code);
|
||||
const globalCode = split.quasiGlobal;
|
||||
const classCode = performCodeSurgery(split.classCode);
|
||||
|
||||
this.setupCodeInjection(src, uid, globalCode, classCode, rerender);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form the final, perfectly valid JS module code, and create the <script>
|
||||
* element for it, to be inserted into the shadow DOM during render().
|
||||
*/
|
||||
setupCodeInjection(src, uid, globalCode, classCode, rerender) {
|
||||
const width = this.getAttribute(`width`, 200);
|
||||
const height = this.getAttribute(`height`, 200);
|
||||
|
||||
this.code = `
|
||||
/**
|
||||
* Program source: ${src}
|
||||
* Data attributes: ${JSON.stringify(this.dataset)}
|
||||
*/
|
||||
${IMPORT_GLOBALS_FROM_GRAPHICS_API};
|
||||
|
||||
${globalCode}
|
||||
|
||||
class Example extends GraphicsAPI {
|
||||
${classCode}
|
||||
}
|
||||
|
||||
new Example('${uid}', ${width}, ${height});
|
||||
`;
|
||||
|
||||
const script = (this.script = document.createElement(`script`));
|
||||
script.type = "module";
|
||||
script.src = `data:application/javascript;charset=utf-8,${encodeURIComponent(this.code)}`;
|
||||
|
||||
if (rerender) this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload this graphics element the brute force way.
|
||||
*/
|
||||
async reset() {
|
||||
const parent = this.parentNode;
|
||||
const offDOM = document.createElement(`div`);
|
||||
offDOM.style.display = `none`;
|
||||
offDOM.innerHTML = this.originalHTML;
|
||||
const newElement = offDOM.querySelector(`graphics-element`);
|
||||
newElement.addEventListener(`load`, () => {
|
||||
parent.replaceChild(newElement, this);
|
||||
document.body.removeChild(offDOM);
|
||||
});
|
||||
document.body.appendChild(offDOM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hand the <graphics-element> a reference to the "Example" instance that it built.
|
||||
*/
|
||||
setGraphic(apiInstance) {
|
||||
this.apiInstance = apiInstance;
|
||||
this.setCanvas(apiInstance.canvas);
|
||||
// at this point we can remove our placeholder image for this element, too.
|
||||
let fallback = this.querySelector(`fallback-image`);
|
||||
if (fallback) this.removeChild(fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locally bind the Example's canvas, since it needs to get added to the shadow DOM.
|
||||
*/
|
||||
setCanvas(canvas) {
|
||||
this.canvas = canvas;
|
||||
// Make sure focus works, Which is a CLUSTERFUCK OF BULLSHIT in the August, 2020,
|
||||
// browser landscape, so this is the best I can give you right now. I am more
|
||||
// disappointed about this than you will ever be.
|
||||
this.canvas.setAttribute(`tabindex`, `0`);
|
||||
this.canvas.addEventListener(`touchstart`, (evt) => this.canvas.focus());
|
||||
this.canvas.addEventListener(`mousedown`, (evt) => this.canvas.focus());
|
||||
// If we get here, there were no source code errors: undo the scheduled error print.
|
||||
clearTimeout(this.errorPrintTimeout);
|
||||
this.render();
|
||||
// Once we've rendered, we can send the "ready for use" signal.
|
||||
this.dispatchEvent(new CustomEvent(`load`));
|
||||
if (this.onload) {
|
||||
this.onload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper to aid debugging, mostly because dev tools are not super
|
||||
* great at pointing you to the right line for an injected script that it
|
||||
* can't actually find anywhere in the document or shadow DOM...
|
||||
*/
|
||||
printCodeDueToError() {
|
||||
debugLog(
|
||||
this.code
|
||||
.split(`\n`)
|
||||
.map((l, pos) => `${pos + 1}: ${l}`)
|
||||
.join(`\n`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the shadow DOM content.
|
||||
*/
|
||||
render() {
|
||||
super.render();
|
||||
|
||||
if (this.script) {
|
||||
if (!this.script.__inserted) {
|
||||
// Schedule an error print, which will get cleared if there
|
||||
// were no source code errors.
|
||||
this.errorPrintTimeout = setTimeout(() => this.printCodeDueToError(), 1000);
|
||||
this.script.__inserted = true;
|
||||
this._shadow.appendChild(this.script);
|
||||
}
|
||||
}
|
||||
|
||||
const slotParent = this._slot.parentNode;
|
||||
if (this.canvas) slotParent.insertBefore(this.canvas, this._slot);
|
||||
if (this.label) slotParent.insertBefore(this.label, this._slot);
|
||||
|
||||
const toptitle = document.createElement(`div`);
|
||||
toptitle.classList.add(`top-title`);
|
||||
|
||||
const r = document.createElement(`button`);
|
||||
r.classList.add(`reset`);
|
||||
r.textContent = this.getAttribute(`reset`) || `reset`;
|
||||
r.addEventListener(`click`, () => this.reset());
|
||||
toptitle.append(r);
|
||||
|
||||
if (this.src) {
|
||||
const a = document.createElement(`a`);
|
||||
a.classList.add(`view-source`);
|
||||
a.textContent = this.getAttribute(`viewSource`) || `view source`;
|
||||
a.href = this.src;
|
||||
a.target = `_blank`;
|
||||
toptitle.append(a);
|
||||
}
|
||||
|
||||
if (this.label) slotParent.insertBefore(toptitle, this.canvas);
|
||||
}
|
||||
}
|
||||
|
||||
// Register our custom element
|
||||
CustomElement.register(GraphicsElement);
|
||||
|
||||
// static property to regular debugging
|
||||
GraphicsElement.DEBUG = false;
|
||||
|
||||
// debugging should be behind a flag
|
||||
function debugLog(...data) {
|
||||
if (GraphicsElement.DEBUG) {
|
||||
console.log(...data);
|
||||
}
|
||||
}
|
||||
|
||||
export { GraphicsElement };
|
2115
docs/js/graphics-element/lib/bezierjs/bezier.js
Normal file
2115
docs/js/graphics-element/lib/bezierjs/bezier.js
Normal file
File diff suppressed because it is too large
Load Diff
226
docs/js/graphics-element/lib/bezierjs/normalise-svg.js
Normal file
226
docs/js/graphics-element/lib/bezierjs/normalise-svg.js
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Normalise an SVG path to absolute coordinates
|
||||
* and full commands, rather than relative coordinates
|
||||
* and/or shortcut commands.
|
||||
*/
|
||||
export default function normalizePath(d) {
|
||||
// preprocess "d" so that we have spaces between values
|
||||
d = d
|
||||
.replace(/,/g, " ") // replace commas with spaces
|
||||
.replace(/-/g, " - ") // add spacing around minus signs
|
||||
.replace(/-\s+/g, "-") // remove spacing to the right of minus signs.
|
||||
.replace(/([a-zA-Z])/g, " $1 ");
|
||||
|
||||
// set up the variables used in this function
|
||||
const instructions = d.replace(/([a-zA-Z])\s?/g, "|$1").split("|"),
|
||||
instructionLength = instructions.length;
|
||||
|
||||
let i,
|
||||
instruction,
|
||||
op,
|
||||
lop,
|
||||
args = [],
|
||||
alen,
|
||||
a,
|
||||
sx = 0,
|
||||
sy = 0,
|
||||
x = 0,
|
||||
y = 0,
|
||||
cx = 0,
|
||||
cy = 0,
|
||||
cx2 = 0,
|
||||
cy2 = 0,
|
||||
rx = 0,
|
||||
ry = 0,
|
||||
xrot = 0,
|
||||
lflag = 0,
|
||||
sweep = 0,
|
||||
normalized = "";
|
||||
|
||||
// we run through the instruction list starting at 1, not 0,
|
||||
// because we split up "|M x y ...." so the first element will
|
||||
// always be an empty string. By design.
|
||||
for (i = 1; i < instructionLength; i++) {
|
||||
// which instruction is this?
|
||||
instruction = instructions[i];
|
||||
op = instruction.substring(0, 1);
|
||||
lop = op.toLowerCase();
|
||||
|
||||
// what are the arguments? note that we need to convert
|
||||
// all strings into numbers, or + will do silly things.
|
||||
args = instruction.replace(op, "").trim().split(" ");
|
||||
args = args
|
||||
.filter(function (v) {
|
||||
return v !== "";
|
||||
})
|
||||
.map(parseFloat);
|
||||
alen = args.length;
|
||||
|
||||
// we could use a switch, but elaborate code in a "case" with
|
||||
// fallthrough is just horrid to read. So let's use ifthen
|
||||
// statements instead.
|
||||
|
||||
// moveto command (plus possible lineto)
|
||||
if (lop === "m") {
|
||||
normalized += "M ";
|
||||
if (op === "m") {
|
||||
x += args[0];
|
||||
y += args[1];
|
||||
} else {
|
||||
x = args[0];
|
||||
y = args[1];
|
||||
}
|
||||
// records start position, for dealing
|
||||
// with the shape close operator ('Z')
|
||||
sx = x;
|
||||
sy = y;
|
||||
normalized += x + " " + y + " ";
|
||||
if (alen > 2) {
|
||||
for (a = 0; a < alen; a += 2) {
|
||||
if (op === "m") {
|
||||
x += args[a];
|
||||
y += args[a + 1];
|
||||
} else {
|
||||
x = args[a];
|
||||
y = args[a + 1];
|
||||
}
|
||||
normalized += "L " + x + " " + y + " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lineto commands
|
||||
else if (lop === "l") {
|
||||
for (a = 0; a < alen; a += 2) {
|
||||
if (op === "l") {
|
||||
x += args[a];
|
||||
y += args[a + 1];
|
||||
} else {
|
||||
x = args[a];
|
||||
y = args[a + 1];
|
||||
}
|
||||
normalized += "L " + x + " " + y + " ";
|
||||
}
|
||||
} else if (lop === "h") {
|
||||
for (a = 0; a < alen; a++) {
|
||||
if (op === "h") {
|
||||
x += args[a];
|
||||
} else {
|
||||
x = args[a];
|
||||
}
|
||||
normalized += "L " + x + " " + y + " ";
|
||||
}
|
||||
} else if (lop === "v") {
|
||||
for (a = 0; a < alen; a++) {
|
||||
if (op === "v") {
|
||||
y += args[a];
|
||||
} else {
|
||||
y = args[a];
|
||||
}
|
||||
normalized += "L " + x + " " + y + " ";
|
||||
}
|
||||
}
|
||||
|
||||
// quadratic curveto commands
|
||||
else if (lop === "q") {
|
||||
for (a = 0; a < alen; a += 4) {
|
||||
if (op === "q") {
|
||||
cx = x + args[a];
|
||||
cy = y + args[a + 1];
|
||||
x += args[a + 2];
|
||||
y += args[a + 3];
|
||||
} else {
|
||||
cx = args[a];
|
||||
cy = args[a + 1];
|
||||
x = args[a + 2];
|
||||
y = args[a + 3];
|
||||
}
|
||||
normalized += "Q " + cx + " " + cy + " " + x + " " + y + " ";
|
||||
}
|
||||
} else if (lop === "t") {
|
||||
for (a = 0; a < alen; a += 2) {
|
||||
// reflect previous cx/cy over x/y
|
||||
cx = x + (x - cx);
|
||||
cy = y + (y - cy);
|
||||
// then get real end point
|
||||
if (op === "t") {
|
||||
x += args[a];
|
||||
y += args[a + 1];
|
||||
} else {
|
||||
x = args[a];
|
||||
y = args[a + 1];
|
||||
}
|
||||
normalized += "Q " + cx + " " + cy + " " + x + " " + y + " ";
|
||||
}
|
||||
}
|
||||
|
||||
// cubic curveto commands
|
||||
else if (lop === "c") {
|
||||
for (a = 0; a < alen; a += 6) {
|
||||
if (op === "c") {
|
||||
cx = x + args[a];
|
||||
cy = y + args[a + 1];
|
||||
cx2 = x + args[a + 2];
|
||||
cy2 = y + args[a + 3];
|
||||
x += args[a + 4];
|
||||
y += args[a + 5];
|
||||
} else {
|
||||
cx = args[a];
|
||||
cy = args[a + 1];
|
||||
cx2 = args[a + 2];
|
||||
cy2 = args[a + 3];
|
||||
x = args[a + 4];
|
||||
y = args[a + 5];
|
||||
}
|
||||
normalized += "C " + cx + " " + cy + " " + cx2 + " " + cy2 + " " + x + " " + y + " ";
|
||||
}
|
||||
} else if (lop === "s") {
|
||||
for (a = 0; a < alen; a += 4) {
|
||||
// reflect previous cx2/cy2 over x/y
|
||||
cx = x + (x - cx2);
|
||||
cy = y + (y - cy2);
|
||||
// then get real control and end point
|
||||
if (op === "s") {
|
||||
cx2 = x + args[a];
|
||||
cy2 = y + args[a + 1];
|
||||
x += args[a + 2];
|
||||
y += args[a + 3];
|
||||
} else {
|
||||
cx2 = args[a];
|
||||
cy2 = args[a + 1];
|
||||
x = args[a + 2];
|
||||
y = args[a + 3];
|
||||
}
|
||||
normalized += "C " + cx + " " + cy + " " + cx2 + " " + cy2 + " " + x + " " + y + " ";
|
||||
}
|
||||
}
|
||||
|
||||
// rx ry x-axis-rotation large-arc-flag sweep-flag x y
|
||||
// a 25,25 -30 0, 1 50,-25
|
||||
|
||||
// arc command
|
||||
else if (lop === "a") {
|
||||
for (a = 0; a < alen; a += 7) {
|
||||
rx = args[a];
|
||||
ry = args[a + 1];
|
||||
xrot = args[a + 2];
|
||||
lflag = args[a + 3];
|
||||
sweep = args[a + 4];
|
||||
if (op === "a") {
|
||||
x += args[a + 5];
|
||||
y += args[a + 6];
|
||||
} else {
|
||||
x = args[a + 5];
|
||||
y = args[a + 6];
|
||||
}
|
||||
normalized += "A " + rx + " " + ry + " " + xrot + " " + lflag + " " + sweep + " " + x + " " + y + " ";
|
||||
}
|
||||
} else if (lop === "z") {
|
||||
normalized += "Z ";
|
||||
// not unimportant: path closing changes the current x/y coordinate
|
||||
x = sx;
|
||||
y = sy;
|
||||
}
|
||||
}
|
||||
return normalized.trim();
|
||||
}
|
70
docs/js/graphics-element/lib/bezierjs/poly-bezier.js
Normal file
70
docs/js/graphics-element/lib/bezierjs/poly-bezier.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { utils } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Poly Bezier
|
||||
* @param {[type]} curves [description]
|
||||
*/
|
||||
class PolyBezier {
|
||||
constructor(curves) {
|
||||
this.curves = [];
|
||||
this._3d = false;
|
||||
if (!!curves) {
|
||||
this.curves = curves;
|
||||
this._3d = this.curves[0]._3d;
|
||||
}
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
toString() {
|
||||
return (
|
||||
"[" +
|
||||
this.curves
|
||||
.map(function (curve) {
|
||||
return utils.pointsToString(curve.points);
|
||||
})
|
||||
.join(", ") +
|
||||
"]"
|
||||
);
|
||||
}
|
||||
|
||||
addCurve(curve) {
|
||||
this.curves.push(curve);
|
||||
this._3d = this._3d || curve._3d;
|
||||
}
|
||||
|
||||
length() {
|
||||
return this.curves
|
||||
.map(function (v) {
|
||||
return v.length();
|
||||
})
|
||||
.reduce(function (a, b) {
|
||||
return a + b;
|
||||
});
|
||||
}
|
||||
|
||||
curve(idx) {
|
||||
return this.curves[idx];
|
||||
}
|
||||
|
||||
bbox() {
|
||||
const c = this.curves;
|
||||
var bbox = c[0].bbox();
|
||||
for (var i = 1; i < c.length; i++) {
|
||||
utils.expandbox(bbox, c[i].bbox());
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
offset(d) {
|
||||
const offset = [];
|
||||
this.curves.forEach(function (v) {
|
||||
offset = offset.concat(v.offset(d));
|
||||
});
|
||||
return new PolyBezier(offset);
|
||||
}
|
||||
}
|
||||
|
||||
export { PolyBezier };
|
45
docs/js/graphics-element/lib/bezierjs/svg-to-beziers.js
Normal file
45
docs/js/graphics-element/lib/bezierjs/svg-to-beziers.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import normalise from "./normalise-svg.js";
|
||||
|
||||
let M = { x: false, y: false };
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
function makeBezier(Bezier, term, values) {
|
||||
if (term === "Z") return;
|
||||
if (term === "M") {
|
||||
M = { x: values[0], y: values[1] };
|
||||
return;
|
||||
}
|
||||
const curve = new Bezier(M.x, M.y, ...values);
|
||||
const last = values.slice(-2);
|
||||
M = { x: last[0], y: last[1] };
|
||||
return curve;
|
||||
}
|
||||
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
function convertPath(Bezier, d) {
|
||||
const terms = normalise(d).split(" "),
|
||||
matcher = new RegExp("[MLCQZ]", "");
|
||||
|
||||
let term,
|
||||
segment,
|
||||
values,
|
||||
segments = [],
|
||||
ARGS = { C: 6, Q: 4, L: 2, M: 2 };
|
||||
|
||||
while (terms.length) {
|
||||
term = terms.splice(0, 1)[0];
|
||||
if (matcher.test(term)) {
|
||||
values = terms.splice(0, ARGS[term]).map(parseFloat);
|
||||
segment = makeBezier(Bezier, term, values);
|
||||
if (segment) segments.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
return new Bezier.PolyBezier(segments);
|
||||
}
|
||||
|
||||
export { convertPath };
|
872
docs/js/graphics-element/lib/bezierjs/utils.js
Normal file
872
docs/js/graphics-element/lib/bezierjs/utils.js
Normal file
@@ -0,0 +1,872 @@
|
||||
import { Bezier } from "./bezier.js";
|
||||
|
||||
// math-inlining.
|
||||
const { abs, cos, sin, acos, atan2, sqrt, pow } = Math;
|
||||
|
||||
// cube root function yielding real roots
|
||||
function crt(v) {
|
||||
return v < 0 ? -pow(-v, 1 / 3) : pow(v, 1 / 3);
|
||||
}
|
||||
|
||||
// trig constants
|
||||
const pi = Math.PI,
|
||||
tau = 2 * pi,
|
||||
quart = pi / 2,
|
||||
// float precision significant decimal
|
||||
epsilon = 0.000001,
|
||||
// extremas used in bbox calculation and similar algorithms
|
||||
nMax = Number.MAX_SAFE_INTEGER || 9007199254740991,
|
||||
nMin = Number.MIN_SAFE_INTEGER || -9007199254740991,
|
||||
// a zero coordinate, which is surprisingly useful
|
||||
ZERO = { x: 0, y: 0, z: 0 };
|
||||
|
||||
// Bezier utility functions
|
||||
const utils = {
|
||||
// Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x))
|
||||
Tvalues: [
|
||||
-0.0640568928626056260850430826247450385909,
|
||||
0.0640568928626056260850430826247450385909,
|
||||
-0.1911188674736163091586398207570696318404,
|
||||
0.1911188674736163091586398207570696318404,
|
||||
-0.3150426796961633743867932913198102407864,
|
||||
0.3150426796961633743867932913198102407864,
|
||||
-0.4337935076260451384870842319133497124524,
|
||||
0.4337935076260451384870842319133497124524,
|
||||
-0.5454214713888395356583756172183723700107,
|
||||
0.5454214713888395356583756172183723700107,
|
||||
-0.6480936519369755692524957869107476266696,
|
||||
0.6480936519369755692524957869107476266696,
|
||||
-0.7401241915785543642438281030999784255232,
|
||||
0.7401241915785543642438281030999784255232,
|
||||
-0.8200019859739029219539498726697452080761,
|
||||
0.8200019859739029219539498726697452080761,
|
||||
-0.8864155270044010342131543419821967550873,
|
||||
0.8864155270044010342131543419821967550873,
|
||||
-0.9382745520027327585236490017087214496548,
|
||||
0.9382745520027327585236490017087214496548,
|
||||
-0.9747285559713094981983919930081690617411,
|
||||
0.9747285559713094981983919930081690617411,
|
||||
-0.9951872199970213601799974097007368118745,
|
||||
0.9951872199970213601799974097007368118745,
|
||||
],
|
||||
|
||||
// Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article)
|
||||
Cvalues: [
|
||||
0.1279381953467521569740561652246953718517,
|
||||
0.1279381953467521569740561652246953718517,
|
||||
0.1258374563468282961213753825111836887264,
|
||||
0.1258374563468282961213753825111836887264,
|
||||
0.121670472927803391204463153476262425607,
|
||||
0.121670472927803391204463153476262425607,
|
||||
0.1155056680537256013533444839067835598622,
|
||||
0.1155056680537256013533444839067835598622,
|
||||
0.1074442701159656347825773424466062227946,
|
||||
0.1074442701159656347825773424466062227946,
|
||||
0.0976186521041138882698806644642471544279,
|
||||
0.0976186521041138882698806644642471544279,
|
||||
0.086190161531953275917185202983742667185,
|
||||
0.086190161531953275917185202983742667185,
|
||||
0.0733464814110803057340336152531165181193,
|
||||
0.0733464814110803057340336152531165181193,
|
||||
0.0592985849154367807463677585001085845412,
|
||||
0.0592985849154367807463677585001085845412,
|
||||
0.0442774388174198061686027482113382288593,
|
||||
0.0442774388174198061686027482113382288593,
|
||||
0.0285313886289336631813078159518782864491,
|
||||
0.0285313886289336631813078159518782864491,
|
||||
0.0123412297999871995468056670700372915759,
|
||||
0.0123412297999871995468056670700372915759,
|
||||
],
|
||||
|
||||
arcfn: function (t, derivativeFn) {
|
||||
const d = derivativeFn(t);
|
||||
let l = d.x * d.x + d.y * d.y;
|
||||
if (typeof d.z !== "undefined") {
|
||||
l += d.z * d.z;
|
||||
}
|
||||
return sqrt(l);
|
||||
},
|
||||
|
||||
compute: function (t, points, _3d) {
|
||||
// shortcuts
|
||||
if (t === 0) {
|
||||
points[0].t = 0;
|
||||
return points[0];
|
||||
}
|
||||
|
||||
const order = points.length - 1;
|
||||
|
||||
if (t === 1) {
|
||||
points[order].t = 1;
|
||||
return points[order];
|
||||
}
|
||||
|
||||
const mt = 1 - t;
|
||||
let p = points;
|
||||
|
||||
// constant?
|
||||
if (order === 0) {
|
||||
points[0].t = t;
|
||||
return points[0];
|
||||
}
|
||||
|
||||
// linear?
|
||||
if (order === 1) {
|
||||
const ret = {
|
||||
x: mt * p[0].x + t * p[1].x,
|
||||
y: mt * p[0].y + t * p[1].y,
|
||||
t: t,
|
||||
};
|
||||
if (_3d) {
|
||||
ret.z = mt * p[0].z + t * p[1].z;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// quadratic/cubic curve?
|
||||
if (order < 4) {
|
||||
let mt2 = mt * mt,
|
||||
t2 = t * t,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d = 0;
|
||||
if (order === 2) {
|
||||
p = [p[0], p[1], p[2], ZERO];
|
||||
a = mt2;
|
||||
b = mt * t * 2;
|
||||
c = t2;
|
||||
} else if (order === 3) {
|
||||
a = mt2 * mt;
|
||||
b = mt2 * t * 3;
|
||||
c = mt * t2 * 3;
|
||||
d = t * t2;
|
||||
}
|
||||
const ret = {
|
||||
x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
|
||||
y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y,
|
||||
t: t,
|
||||
};
|
||||
if (_3d) {
|
||||
ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// higher order curves: use de Casteljau's computation
|
||||
const dCpts = JSON.parse(JSON.stringify(points));
|
||||
while (dCpts.length > 1) {
|
||||
for (let i = 0; i < dCpts.length - 1; i++) {
|
||||
dCpts[i] = {
|
||||
x: dCpts[i].x + (dCpts[i + 1].x - dCpts[i].x) * t,
|
||||
y: dCpts[i].y + (dCpts[i + 1].y - dCpts[i].y) * t,
|
||||
};
|
||||
if (typeof dCpts[i].z !== "undefined") {
|
||||
dCpts[i] = dCpts[i].z + (dCpts[i + 1].z - dCpts[i].z) * t;
|
||||
}
|
||||
}
|
||||
dCpts.splice(dCpts.length - 1, 1);
|
||||
}
|
||||
dCpts[0].t = t;
|
||||
return dCpts[0];
|
||||
},
|
||||
|
||||
computeWithRatios: function (t, points, ratios, _3d) {
|
||||
const mt = 1 - t,
|
||||
r = ratios,
|
||||
p = points;
|
||||
|
||||
let f1 = r[0],
|
||||
f2 = r[1],
|
||||
f3 = r[2],
|
||||
f4 = r[3],
|
||||
d;
|
||||
|
||||
// spec for linear
|
||||
f1 *= mt;
|
||||
f2 *= t;
|
||||
|
||||
if (p.length === 2) {
|
||||
d = f1 + f2;
|
||||
return {
|
||||
x: (f1 * p[0].x + f2 * p[1].x) / d,
|
||||
y: (f1 * p[0].y + f2 * p[1].y) / d,
|
||||
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z) / d,
|
||||
t: t,
|
||||
};
|
||||
}
|
||||
|
||||
// upgrade to quadratic
|
||||
f1 *= mt;
|
||||
f2 *= 2 * mt;
|
||||
f3 *= t * t;
|
||||
|
||||
if (p.length === 3) {
|
||||
d = f1 + f2 + f3;
|
||||
return {
|
||||
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x) / d,
|
||||
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y) / d,
|
||||
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z) / d,
|
||||
t: t,
|
||||
};
|
||||
}
|
||||
|
||||
// upgrade to cubic
|
||||
f1 *= mt;
|
||||
f2 *= 1.5 * mt;
|
||||
f3 *= 3 * mt;
|
||||
f4 *= t * t * t;
|
||||
|
||||
if (p.length === 4) {
|
||||
d = f1 + f2 + f3 + f4;
|
||||
return {
|
||||
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x + f4 * p[3].x) / d,
|
||||
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y + f4 * p[3].y) / d,
|
||||
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z) / d,
|
||||
t: t,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
derive: function (points, _3d) {
|
||||
const dpoints = [];
|
||||
for (let p = points, d = p.length, c = d - 1; d > 1; d--, c--) {
|
||||
const list = [];
|
||||
for (let j = 0, dpt; j < c; j++) {
|
||||
dpt = {
|
||||
x: c * (p[j + 1].x - p[j].x),
|
||||
y: c * (p[j + 1].y - p[j].y),
|
||||
};
|
||||
if (_3d) {
|
||||
dpt.z = c * (p[j + 1].z - p[j].z);
|
||||
}
|
||||
list.push(dpt);
|
||||
}
|
||||
dpoints.push(list);
|
||||
p = list;
|
||||
}
|
||||
return dpoints;
|
||||
},
|
||||
|
||||
between: function (v, m, M) {
|
||||
return (m <= v && v <= M) || utils.approximately(v, m) || utils.approximately(v, M);
|
||||
},
|
||||
|
||||
approximately: function (a, b, precision) {
|
||||
return abs(a - b) <= (precision || epsilon);
|
||||
},
|
||||
|
||||
length: function (derivativeFn) {
|
||||
const z = 0.5,
|
||||
len = utils.Tvalues.length;
|
||||
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0, t; i < len; i++) {
|
||||
t = z * utils.Tvalues[i] + z;
|
||||
sum += utils.Cvalues[i] * utils.arcfn(t, derivativeFn);
|
||||
}
|
||||
return z * sum;
|
||||
},
|
||||
|
||||
map: function (v, ds, de, ts, te) {
|
||||
const d1 = de - ds,
|
||||
d2 = te - ts,
|
||||
v2 = v - ds,
|
||||
r = v2 / d1;
|
||||
return ts + d2 * r;
|
||||
},
|
||||
|
||||
lerp: function (r, v1, v2) {
|
||||
const ret = {
|
||||
x: v1.x + r * (v2.x - v1.x),
|
||||
y: v1.y + r * (v2.y - v1.y),
|
||||
};
|
||||
if (!!v1.z && !!v2.z) {
|
||||
ret.z = v1.z + r * (v2.z - v1.z);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
pointToString: function (p) {
|
||||
let s = p.x + "/" + p.y;
|
||||
if (typeof p.z !== "undefined") {
|
||||
s += "/" + p.z;
|
||||
}
|
||||
return s;
|
||||
},
|
||||
|
||||
pointsToString: function (points) {
|
||||
return "[" + points.map(utils.pointToString).join(", ") + "]";
|
||||
},
|
||||
|
||||
copy: function (obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
},
|
||||
|
||||
angle: function (o, v1, v2) {
|
||||
const dx1 = v1.x - o.x,
|
||||
dy1 = v1.y - o.y,
|
||||
dx2 = v2.x - o.x,
|
||||
dy2 = v2.y - o.y,
|
||||
cross = dx1 * dy2 - dy1 * dx2,
|
||||
dot = dx1 * dx2 + dy1 * dy2;
|
||||
return atan2(cross, dot);
|
||||
},
|
||||
|
||||
// round as string, to avoid rounding errors
|
||||
round: function (v, d) {
|
||||
const s = "" + v;
|
||||
const pos = s.indexOf(".");
|
||||
return parseFloat(s.substring(0, pos + 1 + d));
|
||||
},
|
||||
|
||||
dist: function (p1, p2) {
|
||||
const dx = p1.x - p2.x,
|
||||
dy = p1.y - p2.y;
|
||||
return sqrt(dx * dx + dy * dy);
|
||||
},
|
||||
|
||||
closest: function (LUT, point) {
|
||||
let mdist = pow(2, 63),
|
||||
mpos,
|
||||
d;
|
||||
LUT.forEach(function (p, idx) {
|
||||
d = utils.dist(point, p);
|
||||
if (d < mdist) {
|
||||
mdist = d;
|
||||
mpos = idx;
|
||||
}
|
||||
});
|
||||
return { mdist: mdist, mpos: mpos };
|
||||
},
|
||||
|
||||
abcratio: function (t, n) {
|
||||
// see ratio(t) note on http://pomax.github.io/bezierinfo/#abc
|
||||
if (n !== 2 && n !== 3) {
|
||||
return false;
|
||||
}
|
||||
if (typeof t === "undefined") {
|
||||
t = 0.5;
|
||||
} else if (t === 0 || t === 1) {
|
||||
return t;
|
||||
}
|
||||
const bottom = pow(t, n) + pow(1 - t, n),
|
||||
top = bottom - 1;
|
||||
return abs(top / bottom);
|
||||
},
|
||||
|
||||
projectionratio: function (t, n) {
|
||||
// see u(t) note on http://pomax.github.io/bezierinfo/#abc
|
||||
if (n !== 2 && n !== 3) {
|
||||
return false;
|
||||
}
|
||||
if (typeof t === "undefined") {
|
||||
t = 0.5;
|
||||
} else if (t === 0 || t === 1) {
|
||||
return t;
|
||||
}
|
||||
const top = pow(1 - t, n),
|
||||
bottom = pow(t, n) + top;
|
||||
return top / bottom;
|
||||
},
|
||||
|
||||
lli8: function (x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||
const nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4),
|
||||
ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4),
|
||||
d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
||||
if (d == 0) {
|
||||
return false;
|
||||
}
|
||||
return { x: nx / d, y: ny / d };
|
||||
},
|
||||
|
||||
lli4: function (p1, p2, p3, p4) {
|
||||
const x1 = p1.x,
|
||||
y1 = p1.y,
|
||||
x2 = p2.x,
|
||||
y2 = p2.y,
|
||||
x3 = p3.x,
|
||||
y3 = p3.y,
|
||||
x4 = p4.x,
|
||||
y4 = p4.y;
|
||||
return utils.lli8(x1, y1, x2, y2, x3, y3, x4, y4);
|
||||
},
|
||||
|
||||
lli: function (v1, v2) {
|
||||
return utils.lli4(v1, v1.c, v2, v2.c);
|
||||
},
|
||||
|
||||
makeline: function (p1, p2) {
|
||||
const x1 = p1.x,
|
||||
y1 = p1.y,
|
||||
x2 = p2.x,
|
||||
y2 = p2.y,
|
||||
dx = (x2 - x1) / 3,
|
||||
dy = (y2 - y1) / 3;
|
||||
return new Bezier(x1, y1, x1 + dx, y1 + dy, x1 + 2 * dx, y1 + 2 * dy, x2, y2);
|
||||
},
|
||||
|
||||
findbbox: function (sections) {
|
||||
let mx = nMax,
|
||||
my = nMax,
|
||||
MX = nMin,
|
||||
MY = nMin;
|
||||
sections.forEach(function (s) {
|
||||
const bbox = s.bbox();
|
||||
if (mx > bbox.x.min) mx = bbox.x.min;
|
||||
if (my > bbox.y.min) my = bbox.y.min;
|
||||
if (MX < bbox.x.max) MX = bbox.x.max;
|
||||
if (MY < bbox.y.max) MY = bbox.y.max;
|
||||
});
|
||||
return {
|
||||
x: { min: mx, mid: (mx + MX) / 2, max: MX, size: MX - mx },
|
||||
y: { min: my, mid: (my + MY) / 2, max: MY, size: MY - my },
|
||||
};
|
||||
},
|
||||
|
||||
shapeintersections: function (s1, bbox1, s2, bbox2, curveIntersectionThreshold) {
|
||||
if (!utils.bboxoverlap(bbox1, bbox2)) return [];
|
||||
const intersections = [];
|
||||
const a1 = [s1.startcap, s1.forward, s1.back, s1.endcap];
|
||||
const a2 = [s2.startcap, s2.forward, s2.back, s2.endcap];
|
||||
a1.forEach(function (l1) {
|
||||
if (l1.virtual) return;
|
||||
a2.forEach(function (l2) {
|
||||
if (l2.virtual) return;
|
||||
const iss = l1.intersects(l2, curveIntersectionThreshold);
|
||||
if (iss.length > 0) {
|
||||
iss.c1 = l1;
|
||||
iss.c2 = l2;
|
||||
iss.s1 = s1;
|
||||
iss.s2 = s2;
|
||||
intersections.push(iss);
|
||||
}
|
||||
});
|
||||
});
|
||||
return intersections;
|
||||
},
|
||||
|
||||
makeshape: function (forward, back, curveIntersectionThreshold) {
|
||||
const bpl = back.points.length;
|
||||
const fpl = forward.points.length;
|
||||
const start = utils.makeline(back.points[bpl - 1], forward.points[0]);
|
||||
const end = utils.makeline(forward.points[fpl - 1], back.points[0]);
|
||||
const shape = {
|
||||
startcap: start,
|
||||
forward: forward,
|
||||
back: back,
|
||||
endcap: end,
|
||||
bbox: utils.findbbox([start, forward, back, end]),
|
||||
};
|
||||
shape.intersections = function (s2) {
|
||||
return utils.shapeintersections(shape, shape.bbox, s2, s2.bbox, curveIntersectionThreshold);
|
||||
};
|
||||
return shape;
|
||||
},
|
||||
|
||||
getminmax: function (curve, d, list) {
|
||||
if (!list) return { min: 0, max: 0 };
|
||||
let min = nMax,
|
||||
max = nMin,
|
||||
t,
|
||||
c;
|
||||
if (list.indexOf(0) === -1) {
|
||||
list = [0].concat(list);
|
||||
}
|
||||
if (list.indexOf(1) === -1) {
|
||||
list.push(1);
|
||||
}
|
||||
for (let i = 0, len = list.length; i < len; i++) {
|
||||
t = list[i];
|
||||
c = curve.get(t);
|
||||
if (c[d] < min) {
|
||||
min = c[d];
|
||||
}
|
||||
if (c[d] > max) {
|
||||
max = c[d];
|
||||
}
|
||||
}
|
||||
return { min: min, mid: (min + max) / 2, max: max, size: max - min };
|
||||
},
|
||||
|
||||
align: function (points, line) {
|
||||
const tx = line.p1.x,
|
||||
ty = line.p1.y,
|
||||
a = -atan2(line.p2.y - ty, line.p2.x - tx),
|
||||
d = function (v) {
|
||||
return {
|
||||
x: (v.x - tx) * cos(a) - (v.y - ty) * sin(a),
|
||||
y: (v.x - tx) * sin(a) + (v.y - ty) * cos(a),
|
||||
};
|
||||
};
|
||||
return points.map(d);
|
||||
},
|
||||
|
||||
roots: function (points, line) {
|
||||
line = line || { p1: { x: 0, y: 0 }, p2: { x: 1, y: 0 } };
|
||||
|
||||
const order = points.length - 1;
|
||||
const aligned = utils.align(points, line);
|
||||
const reduce = function (t) {
|
||||
return 0 <= t && t <= 1;
|
||||
};
|
||||
|
||||
if (order === 2) {
|
||||
const a = aligned[0].y,
|
||||
b = aligned[1].y,
|
||||
c = aligned[2].y,
|
||||
d = a - 2 * b + c;
|
||||
if (d !== 0) {
|
||||
const m1 = -sqrt(b * b - a * c),
|
||||
m2 = -a + b,
|
||||
v1 = -(m1 + m2) / d,
|
||||
v2 = -(-m1 + m2) / d;
|
||||
return [v1, v2].filter(reduce);
|
||||
} else if (b !== c && d === 0) {
|
||||
return [(2 * b - c) / (2 * b - 2 * c)].filter(reduce);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm
|
||||
const pa = aligned[0].y,
|
||||
pb = aligned[1].y,
|
||||
pc = aligned[2].y,
|
||||
pd = aligned[3].y;
|
||||
|
||||
let d = -pa + 3 * pb - 3 * pc + pd,
|
||||
a = 3 * pa - 6 * pb + 3 * pc,
|
||||
b = -3 * pa + 3 * pb,
|
||||
c = pa;
|
||||
|
||||
if (utils.approximately(d, 0)) {
|
||||
// this is not a cubic curve.
|
||||
if (utils.approximately(a, 0)) {
|
||||
// in fact, this is not a quadratic curve either.
|
||||
if (utils.approximately(b, 0)) {
|
||||
// in fact in fact, there are no solutions.
|
||||
return [];
|
||||
}
|
||||
// linear solution:
|
||||
return [-c / b].filter(reduce);
|
||||
}
|
||||
// quadratic solution:
|
||||
const q = sqrt(b * b - 4 * a * c),
|
||||
a2 = 2 * a;
|
||||
return [(q - b) / a2, (-b - q) / a2].filter(reduce);
|
||||
}
|
||||
|
||||
// at this point, we know we need a cubic solution:
|
||||
|
||||
a /= d;
|
||||
b /= d;
|
||||
c /= d;
|
||||
|
||||
const p = (3 * b - a * a) / 3,
|
||||
p3 = p / 3,
|
||||
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27,
|
||||
q2 = q / 2,
|
||||
discriminant = q2 * q2 + p3 * p3 * p3;
|
||||
|
||||
let u1, v1, x1, x2, x3;
|
||||
if (discriminant < 0) {
|
||||
const mp3 = -p / 3,
|
||||
mp33 = mp3 * mp3 * mp3,
|
||||
r = sqrt(mp33),
|
||||
t = -q / (2 * r),
|
||||
cosphi = t < -1 ? -1 : t > 1 ? 1 : t,
|
||||
phi = acos(cosphi),
|
||||
crtr = crt(r),
|
||||
t1 = 2 * crtr;
|
||||
x1 = t1 * cos(phi / 3) - a / 3;
|
||||
x2 = t1 * cos((phi + tau) / 3) - a / 3;
|
||||
x3 = t1 * cos((phi + 2 * tau) / 3) - a / 3;
|
||||
return [x1, x2, x3].filter(reduce);
|
||||
} else if (discriminant === 0) {
|
||||
u1 = q2 < 0 ? crt(-q2) : -crt(q2);
|
||||
x1 = 2 * u1 - a / 3;
|
||||
x2 = -u1 - a / 3;
|
||||
return [x1, x2].filter(reduce);
|
||||
} else {
|
||||
const sd = sqrt(discriminant);
|
||||
u1 = crt(-q2 + sd);
|
||||
v1 = crt(q2 + sd);
|
||||
return [u1 - v1 - a / 3].filter(reduce);
|
||||
}
|
||||
},
|
||||
|
||||
droots: function (p) {
|
||||
// quadratic roots are easy
|
||||
if (p.length === 3) {
|
||||
const a = p[0],
|
||||
b = p[1],
|
||||
c = p[2],
|
||||
d = a - 2 * b + c;
|
||||
if (d !== 0) {
|
||||
const m1 = -sqrt(b * b - a * c),
|
||||
m2 = -a + b,
|
||||
v1 = -(m1 + m2) / d,
|
||||
v2 = -(-m1 + m2) / d;
|
||||
return [v1, v2];
|
||||
} else if (b !== c && d === 0) {
|
||||
return [(2 * b - c) / (2 * (b - c))];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// linear roots are even easier
|
||||
if (p.length === 2) {
|
||||
const a = p[0],
|
||||
b = p[1];
|
||||
if (a !== b) {
|
||||
return [a / (a - b)];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
curvature: function (t, d1, d2, _3d, kOnly) {
|
||||
let num,
|
||||
dnm,
|
||||
adk,
|
||||
dk,
|
||||
k = 0,
|
||||
r = 0;
|
||||
|
||||
//
|
||||
// We're using the following formula for curvature:
|
||||
//
|
||||
// x'y" - y'x"
|
||||
// k(t) = ------------------
|
||||
// (x'² + y'²)^(3/2)
|
||||
//
|
||||
// from https://en.wikipedia.org/wiki/Radius_of_curvature#Definition
|
||||
//
|
||||
// With it corresponding 3D counterpart:
|
||||
//
|
||||
// sqrt( (y'z" - y"z')² + (z'x" - z"x')² + (x'y" - x"y')²)
|
||||
// k(t) = -------------------------------------------------------
|
||||
// (x'² + y'² + z'²)^(3/2)
|
||||
//
|
||||
|
||||
const d = utils.compute(t, d1);
|
||||
const dd = utils.compute(t, d2);
|
||||
const qdsum = d.x * d.x + d.y * d.y;
|
||||
|
||||
if (_3d) {
|
||||
num = sqrt(pow(d.y * dd.z - dd.y * d.z, 2) + pow(d.z * dd.x - dd.z * d.x, 2) + pow(d.x * dd.y - dd.x * d.y, 2));
|
||||
dnm = pow(qdsum + d.z * d.z, 3 / 2);
|
||||
} else {
|
||||
num = d.x * dd.y - d.y * dd.x;
|
||||
dnm = pow(qdsum, 3 / 2);
|
||||
}
|
||||
|
||||
if (num === 0 || dnm === 0) {
|
||||
return { k: 0, r: 0 };
|
||||
}
|
||||
|
||||
k = num / dnm;
|
||||
r = dnm / num;
|
||||
|
||||
// We're also computing the derivative of kappa, because
|
||||
// there is value in knowing the rate of change for the
|
||||
// curvature along the curve. And we're just going to
|
||||
// ballpark it based on an epsilon.
|
||||
if (!kOnly) {
|
||||
// compute k'(t) based on the interval before, and after it,
|
||||
// to at least try to not introduce forward/backward pass bias.
|
||||
const pk = utils.curvature(t - 0.001, d1, d2, _3d, true).k;
|
||||
const nk = utils.curvature(t + 0.001, d1, d2, _3d, true).k;
|
||||
dk = (nk - k + (k - pk)) / 2;
|
||||
adk = (abs(nk - k) + abs(k - pk)) / 2;
|
||||
}
|
||||
|
||||
return { k: k, r: r, dk: dk, adk: adk };
|
||||
},
|
||||
|
||||
inflections: function (points) {
|
||||
if (points.length < 4) return [];
|
||||
|
||||
// FIXME: TODO: add in inflection abstraction for quartic+ curves?
|
||||
|
||||
const p = utils.align(points, { p1: points[0], p2: points.slice(-1)[0] }),
|
||||
a = p[2].x * p[1].y,
|
||||
b = p[3].x * p[1].y,
|
||||
c = p[1].x * p[2].y,
|
||||
d = p[3].x * p[2].y,
|
||||
v1 = 18 * (-3 * a + 2 * b + 3 * c - d),
|
||||
v2 = 18 * (3 * a - b - 3 * c),
|
||||
v3 = 18 * (c - a);
|
||||
|
||||
if (utils.approximately(v1, 0)) {
|
||||
if (!utils.approximately(v2, 0)) {
|
||||
let t = -v3 / v2;
|
||||
if (0 <= t && t <= 1) return [t];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const trm = v2 * v2 - 4 * v1 * v3,
|
||||
sq = Math.sqrt(trm),
|
||||
d2 = 2 * v1;
|
||||
|
||||
if (utils.approximately(d2, 0)) return [];
|
||||
|
||||
return [(sq - v2) / d2, -(v2 + sq) / d2].filter(function (r) {
|
||||
return 0 <= r && r <= 1;
|
||||
});
|
||||
},
|
||||
|
||||
bboxoverlap: function (b1, b2) {
|
||||
const dims = ["x", "y"],
|
||||
len = dims.length;
|
||||
|
||||
for (let i = 0, dim, l, t, d; i < len; i++) {
|
||||
dim = dims[i];
|
||||
l = b1[dim].mid;
|
||||
t = b2[dim].mid;
|
||||
d = (b1[dim].size + b2[dim].size) / 2;
|
||||
if (abs(l - t) >= d) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
expandbox: function (bbox, _bbox) {
|
||||
if (_bbox.x.min < bbox.x.min) {
|
||||
bbox.x.min = _bbox.x.min;
|
||||
}
|
||||
if (_bbox.y.min < bbox.y.min) {
|
||||
bbox.y.min = _bbox.y.min;
|
||||
}
|
||||
if (_bbox.z && _bbox.z.min < bbox.z.min) {
|
||||
bbox.z.min = _bbox.z.min;
|
||||
}
|
||||
if (_bbox.x.max > bbox.x.max) {
|
||||
bbox.x.max = _bbox.x.max;
|
||||
}
|
||||
if (_bbox.y.max > bbox.y.max) {
|
||||
bbox.y.max = _bbox.y.max;
|
||||
}
|
||||
if (_bbox.z && _bbox.z.max > bbox.z.max) {
|
||||
bbox.z.max = _bbox.z.max;
|
||||
}
|
||||
bbox.x.mid = (bbox.x.min + bbox.x.max) / 2;
|
||||
bbox.y.mid = (bbox.y.min + bbox.y.max) / 2;
|
||||
if (bbox.z) {
|
||||
bbox.z.mid = (bbox.z.min + bbox.z.max) / 2;
|
||||
}
|
||||
bbox.x.size = bbox.x.max - bbox.x.min;
|
||||
bbox.y.size = bbox.y.max - bbox.y.min;
|
||||
if (bbox.z) {
|
||||
bbox.z.size = bbox.z.max - bbox.z.min;
|
||||
}
|
||||
},
|
||||
|
||||
pairiteration: function (c1, c2, curveIntersectionThreshold) {
|
||||
const c1b = c1.bbox(),
|
||||
c2b = c2.bbox(),
|
||||
r = 100000,
|
||||
threshold = curveIntersectionThreshold || 0.5;
|
||||
|
||||
if (c1b.x.size + c1b.y.size < threshold && c2b.x.size + c2b.y.size < threshold) {
|
||||
return [(((r * (c1._t1 + c1._t2)) / 2) | 0) / r + "/" + (((r * (c2._t1 + c2._t2)) / 2) | 0) / r];
|
||||
}
|
||||
|
||||
let cc1 = c1.split(0.5),
|
||||
cc2 = c2.split(0.5),
|
||||
pairs = [
|
||||
{ left: cc1.left, right: cc2.left },
|
||||
{ left: cc1.left, right: cc2.right },
|
||||
{ left: cc1.right, right: cc2.right },
|
||||
{ left: cc1.right, right: cc2.left },
|
||||
];
|
||||
|
||||
pairs = pairs.filter(function (pair) {
|
||||
return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox());
|
||||
});
|
||||
|
||||
let results = [];
|
||||
|
||||
if (pairs.length === 0) return results;
|
||||
|
||||
pairs.forEach(function (pair) {
|
||||
results = results.concat(utils.pairiteration(pair.left, pair.right, threshold));
|
||||
});
|
||||
|
||||
results = results.filter(function (v, i) {
|
||||
return results.indexOf(v) === i;
|
||||
});
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
getccenter: function (p1, p2, p3) {
|
||||
const dx1 = p2.x - p1.x,
|
||||
dy1 = p2.y - p1.y,
|
||||
dx2 = p3.x - p2.x,
|
||||
dy2 = p3.y - p2.y,
|
||||
dx1p = dx1 * cos(quart) - dy1 * sin(quart),
|
||||
dy1p = dx1 * sin(quart) + dy1 * cos(quart),
|
||||
dx2p = dx2 * cos(quart) - dy2 * sin(quart),
|
||||
dy2p = dx2 * sin(quart) + dy2 * cos(quart),
|
||||
// chord midpoints
|
||||
mx1 = (p1.x + p2.x) / 2,
|
||||
my1 = (p1.y + p2.y) / 2,
|
||||
mx2 = (p2.x + p3.x) / 2,
|
||||
my2 = (p2.y + p3.y) / 2,
|
||||
// midpoint offsets
|
||||
mx1n = mx1 + dx1p,
|
||||
my1n = my1 + dy1p,
|
||||
mx2n = mx2 + dx2p,
|
||||
my2n = my2 + dy2p,
|
||||
// intersection of these lines:
|
||||
arc = utils.lli8(mx1, my1, mx1n, my1n, mx2, my2, mx2n, my2n),
|
||||
r = utils.dist(arc, p1);
|
||||
|
||||
// arc start/end values, over mid point:
|
||||
let s = atan2(p1.y - arc.y, p1.x - arc.x),
|
||||
m = atan2(p2.y - arc.y, p2.x - arc.x),
|
||||
e = atan2(p3.y - arc.y, p3.x - arc.x),
|
||||
_;
|
||||
|
||||
// determine arc direction (cw/ccw correction)
|
||||
if (s < e) {
|
||||
// if s<m<e, arc(s, e)
|
||||
// if m<s<e, arc(e, s + tau)
|
||||
// if s<e<m, arc(e, s + tau)
|
||||
if (s > m || m > e) {
|
||||
s += tau;
|
||||
}
|
||||
if (s > e) {
|
||||
_ = e;
|
||||
e = s;
|
||||
s = _;
|
||||
}
|
||||
} else {
|
||||
// if e<m<s, arc(e, s)
|
||||
// if m<e<s, arc(s, e + tau)
|
||||
// if e<s<m, arc(s, e + tau)
|
||||
if (e < m && m < s) {
|
||||
_ = e;
|
||||
e = s;
|
||||
s = _;
|
||||
} else {
|
||||
e += tau;
|
||||
}
|
||||
}
|
||||
// assign and done.
|
||||
arc.s = s;
|
||||
arc.e = e;
|
||||
arc.r = r;
|
||||
return arc;
|
||||
},
|
||||
|
||||
numberSort: function (a, b) {
|
||||
return a - b;
|
||||
},
|
||||
};
|
||||
|
||||
export { utils };
|
27
docs/js/graphics-element/lib/create.js
Normal file
27
docs/js/graphics-element/lib/create.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { enrich } from "./enrich.js";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function create(tag) {
|
||||
if (typeof document !== `undefined`) {
|
||||
return enrich(document.createElement(tag));
|
||||
}
|
||||
|
||||
const element = {
|
||||
name: tag,
|
||||
tag: tag.toUpperCase(),
|
||||
append: noop,
|
||||
appendChild: noop,
|
||||
replaceChild: noop,
|
||||
removeChild: noop,
|
||||
classList: {
|
||||
add: noop,
|
||||
remove: noop,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
export { create };
|
30
docs/js/graphics-element/lib/enrich.js
Normal file
30
docs/js/graphics-element/lib/enrich.js
Normal file
@@ -0,0 +1,30 @@
|
||||
function enrich(element) {
|
||||
if (!element) return element;
|
||||
|
||||
element.__listeners = {};
|
||||
|
||||
element.listen = function (evtNames, handler) {
|
||||
if (!evtNames.map) evtNames = [evtNames];
|
||||
evtNames.forEach((evtName) => {
|
||||
element.addEventListener(evtName, handler);
|
||||
if (!element.__listeners[evtName]) {
|
||||
element.__listeners[evtName] = [];
|
||||
}
|
||||
element.__listeners[evtName].push(handler);
|
||||
});
|
||||
}.bind(element);
|
||||
|
||||
element.ignore = function (evtNames, handler) {
|
||||
if (!evtNames.map) evtNames = [evtNames];
|
||||
evtNames.forEach((evtName) => {
|
||||
if (!handler) {
|
||||
return element.__listeners[evtName].forEach((h) => element.removeEventListener(evtName, h));
|
||||
}
|
||||
element.removeEventListener(evtName, handler);
|
||||
});
|
||||
}.bind(element);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
export { enrich };
|
108
docs/js/graphics-element/lib/lexer.js
Normal file
108
docs/js/graphics-element/lib/lexer.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// TODO: FIXME: finish writing out this functionality
|
||||
|
||||
/**
|
||||
|
||||
Scope 0:
|
||||
|
||||
check for variable declaration/assignment, as well as function
|
||||
declarations _without_ the `function` keyword
|
||||
|
||||
Scope 1+:
|
||||
|
||||
check any not-namespaced function calls to see whether they map
|
||||
to any API functions. If they do, they should be prefixed with
|
||||
`this.`
|
||||
|
||||
check any not-namespaced var references to see whether they map
|
||||
to any predefined API vars. If they do, they should be prefixed
|
||||
with `this.`
|
||||
|
||||
**/
|
||||
|
||||
function splitSymbols(v) {
|
||||
if (v.match(/\w/)) return v;
|
||||
return v.split(``);
|
||||
}
|
||||
|
||||
class Lexer {
|
||||
constructor(code) {
|
||||
this.scope = 0;
|
||||
this.pos = 0;
|
||||
this.tokens = code.split(/\b/).map(splitSymbols).flat();
|
||||
this.scopes = [];
|
||||
console.log(this.tokens);
|
||||
}
|
||||
|
||||
parse() {
|
||||
while (this.pos < this.tokens.length) {
|
||||
let token = this.tokens[this.pos++];
|
||||
|
||||
if ([`const`, `let`, "var"].includes(token)) {
|
||||
this.parseVariable(token);
|
||||
}
|
||||
|
||||
// skip over strings so we don't treat them as active content
|
||||
else if ([`'`, `"`, "`"].includes(token)) {
|
||||
this.parseString(token);
|
||||
}
|
||||
|
||||
// figure out if
|
||||
else if (token === `(`) {
|
||||
let functor,
|
||||
i = 2;
|
||||
do {
|
||||
functor = this.tokens[this.pos - i++];
|
||||
} while (functor.match(/\s+/));
|
||||
|
||||
// TODO: maths is fun?
|
||||
|
||||
console.log(`[${this.scope}]: ${functor}(...`);
|
||||
} else if (token === `)`) {
|
||||
}
|
||||
|
||||
// ...
|
||||
else if (token === `{`) {
|
||||
this.scopes[this.pos] = ++this.scope;
|
||||
}
|
||||
|
||||
// ...
|
||||
else if (token === `}`) {
|
||||
this.scopes[this.pos] = --this.scope;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(this.scopes);
|
||||
}
|
||||
|
||||
parseVariable(type) {
|
||||
let name;
|
||||
do {
|
||||
name = this.tokens[this.pos++];
|
||||
} while (name.match(/\s+/));
|
||||
console.log(`[${this.scope}]: ${type} ${name}`);
|
||||
}
|
||||
|
||||
parseString(symbol) {
|
||||
// we technically don't really care about the contents
|
||||
// of strings, as they don't introduce new variables
|
||||
// or functions that we need to care about.
|
||||
let token;
|
||||
let buffer = [symbol];
|
||||
let blen = 1;
|
||||
do {
|
||||
token = this.tokens[this.pos++];
|
||||
buffer.push(token);
|
||||
blen++;
|
||||
} while (token !== symbol && buffer[blen - 2] !== `\\` && this.pos < this.tokens.length);
|
||||
// buffer = buffer.join(``);
|
||||
// if (symbol === "`") {
|
||||
// this.parseTemplateString(buffer);
|
||||
// }
|
||||
}
|
||||
|
||||
// parseTemplateString(buffer) {
|
||||
// // console.log(buffer);
|
||||
// }
|
||||
}
|
||||
|
||||
export { Lexer };
|
37
docs/js/graphics-element/lib/perform-code-surgery.js
Normal file
37
docs/js/graphics-element/lib/perform-code-surgery.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { GraphicsAPI } from "../api/graphics-api.js";
|
||||
|
||||
export default function performCodeSurgery(code) {
|
||||
// 0. strip out superfluous whitespace
|
||||
|
||||
code = code.replace(/\r?\n(\r?\n)+/, `\n`);
|
||||
|
||||
// 1. ensure that anything that needs to run by first calling its super function, does so.
|
||||
|
||||
GraphicsAPI.superCallers.forEach((name) => {
|
||||
const re = new RegExp(`${name}\\(([^)]*)\\)[\\s\\r\\n]*{[\\s\\r\\n]*`, `g`);
|
||||
code = code.replace(re, `${name}($1) { super.${name}($1);\n`);
|
||||
});
|
||||
|
||||
// 2. rewrite event handlers so that they capture the event and forward it to the super function.
|
||||
|
||||
GraphicsAPI.eventHandlers.forEach((name) => {
|
||||
const re = new RegExp(`\\b${name}\\(\\)[\\s\\r\\n]*{[\\s\\r\\n]*`, `g`);
|
||||
code = code.replace(re, `${name}(evt) { super.${name}(evt);\n`);
|
||||
});
|
||||
|
||||
// 3. rewrite all public GraphicsAPI functions to have the required `this.` prefix
|
||||
|
||||
GraphicsAPI.methods.forEach((fn) => {
|
||||
const re = new RegExp(`([!({\\s\\r\\n])${fn}\\(`, `g`);
|
||||
code = code.replace(re, `$1this.${fn}(`);
|
||||
});
|
||||
|
||||
// 4. do the same for all GraphicsAPI constants.
|
||||
|
||||
GraphicsAPI.constants.forEach((name) => {
|
||||
const re = new RegExp(`(\\b)${name}(\\b)`, `g`);
|
||||
code = code.replace(re, `$1this.${name}$2`);
|
||||
});
|
||||
|
||||
return code;
|
||||
}
|
43
docs/js/graphics-element/lib/split-code-sections.js
Normal file
43
docs/js/graphics-element/lib/split-code-sections.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Get all code that isn't contained in functions.
|
||||
* We're going to regexp our way to flawed victory here.
|
||||
*/
|
||||
export default function splitCodeSections(code) {
|
||||
// removs comments and superfluous white space.
|
||||
code = code.replace(/\\\*[\w\s\r\n]+?\*\\/, ``);
|
||||
code = code.replace(/\r?\n(\r?\n)+/, `\n`);
|
||||
|
||||
const re = /\b[\w\W][^\s]*?\([^)]*\)[\r\n\s]*{/;
|
||||
const cuts = [];
|
||||
for (let result = code.match(re); result; result = code.match(re)) {
|
||||
result = result[0];
|
||||
|
||||
let start = code.indexOf(result);
|
||||
let end = start + result.length;
|
||||
let depth = 0;
|
||||
let slice = Array.from(code).slice(start + result.length);
|
||||
|
||||
slice.some((c, pos) => {
|
||||
if (c === `{`) {
|
||||
depth++;
|
||||
return false;
|
||||
}
|
||||
if (c === `}`) {
|
||||
if (depth > 0) {
|
||||
depth--;
|
||||
return false;
|
||||
}
|
||||
end += pos + 1;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
let cut = code.slice(start, end);
|
||||
cuts.push(cut);
|
||||
code = code.replace(cut, ``);
|
||||
}
|
||||
return {
|
||||
quasiGlobal: code,
|
||||
classCode: cuts.join(`\n`),
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user