1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-27 02:05:34 +02:00

canonical

This commit is contained in:
Pomax
2020-08-21 15:32:49 -07:00
parent 8dce6aed6b
commit c90565f493
227 changed files with 1899 additions and 590 deletions

View File

@@ -1,3 +1,5 @@
import { hatch } from "./util/hatchery.js";
/**
* The base API class, responsible for such things as setting up canvas event
* handling, method accumulation, custom element binding, etc. etc.
@@ -6,7 +8,12 @@
*/
class BaseAPI {
static get privateMethods() {
return [`constructor`, `addListeners`, `getCursorCoords`]
return [
`constructor`,
`createHatchPatterns`,
`addListeners`,
`getCursorCoords`,
]
.concat(this.superCallers)
.concat(this.eventHandlers);
}
@@ -44,6 +51,7 @@ class BaseAPI {
} else {
this.canvas = document.createElement(`canvas`);
}
this.HATCHING = hatch(canvasBuildFunction);
this.addListeners();
this.setSize(width, height);
this.setup();
@@ -201,6 +209,11 @@ class BaseAPI {
setup() {
// console.log(`setup`);
this.movable = [];
this.font = {
size: 10,
weight: 400,
family: `arial`,
};
}
/**
@@ -233,6 +246,9 @@ function enhanceContext(ctx) {
lineWidth: ctx.lineWidth,
textAlign: ctx.textAlign,
transform: [m.a, m.b, m.c, m.d, m.e, m.f],
font: ctx.font,
shadowColor: ctx.shadowColor,
shadowBlur: ctx.shadowColor,
};
styles.push(e);
};

View File

@@ -26,6 +26,12 @@ class GraphicsAPI extends BaseAPI {
`CENTER`,
`LEFT`,
`RIGHT`,
`HATCH1`,
`HATCH2`,
`HATCH3`,
`HATCH4`,
`HATCH5`,
`HATCH6`,
];
}
@@ -64,6 +70,25 @@ class GraphicsAPI extends BaseAPI {
get RIGHT() {
return `right`;
}
// hatching patterns
get HATCH1() {
return this.HATCHING[0];
}
get HATCH2() {
return this.HATCHING[1];
}
get HATCH3() {
return this.HATCHING[2];
}
get HATCH4() {
return this.HATCHING[3];
}
get HATCH5() {
return this.HATCHING[4];
}
get HATCH6() {
return this.HATCHING[5];
}
onMouseDown(evt) {
super.onMouseDown(evt);
@@ -113,6 +138,20 @@ class GraphicsAPI extends BaseAPI {
points.forEach((p) => this.movable.push(p));
}
/**
* 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
*/
@@ -251,6 +290,22 @@ class GraphicsAPI extends BaseAPI {
this.setStroke(false);
}
/**
* 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 context lineWidth
*/
@@ -258,6 +313,52 @@ class GraphicsAPI extends BaseAPI {
this.ctx.lineWidth = `${width}px`;
}
/**
* Set the font size
*/
setFontSize(px) {
this.font.size = px;
this.setFont();
}
/**
* Set the font weight (CSS name/number)
*/
setFontWeight(val) {
this.font.weight = val;
this.setFont();
}
/**
* Set the font family by name
*/
setFontFamily(name) {
this.font.family = name;
this.setFont();
}
setFont(font) {
font =
font || `${this.font.weight} ${this.font.size}px ${this.font.family}`;
this.ctx.font = font;
}
/**
* Set text shadow
*/
setShadow(color, px) {
this.ctx.shadowColor = color;
this.ctx.shadowBlur = px;
}
/**
* Disable text shadow
*/
noShadow() {
this.ctx.shadowColor = `transparent`;
this.ctx.shadowBlur = 0;
}
/**
* Cache all styling values
*/
@@ -319,12 +420,17 @@ class GraphicsAPI extends BaseAPI {
x = x.x;
}
const ctx = this.ctx;
ctx.cacheStyle();
if (alignment) {
ctx.cacheStyle();
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);
if (alignment) ctx.restoreStyle();
ctx.restoreStyle();
}
/**
@@ -339,18 +445,14 @@ class GraphicsAPI extends BaseAPI {
* Draw a function plot from [start] to [end] in [steps] steps.
* Returns the plot shape so that it can be cached for redrawing.
*/
plot(fn, start = 0, end = 1, steps = 24) {
const ctx = this.ctx;
ctx.cacheStyle();
ctx.fillStyle = `transparent`;
plot(fn, start = 0, end = 1, steps = 24, xscale = 1, yscale = 1) {
const interval = end - start;
this.start();
for (let i = 0, e = steps - 1, v; i < steps; i++) {
v = fn(start + (interval * i) / e);
this.vertex(v.x, v.y);
this.vertex(v.x * xscale, v.y * yscale);
}
this.end();
ctx.restoreStyle();
return this.currentShape;
}
@@ -378,9 +480,17 @@ class GraphicsAPI extends BaseAPI {
/**
* Draw a previously created shape
*/
drawShape(shape) {
this.currentShape = shape;
drawShape(...shapes) {
shapes = shapes.map((s) => {
if (s instanceof Shape) return s;
return new Shape(this.POLYGON, undefined, s);
});
this.currentShape = shapes[0].copy();
for (let i = 1; i < shapes.length; i++) {
this.currentShape.merge(shapes[i]);
}
this.end();
this.STARTREPORTING = false;
}
/**
@@ -516,6 +626,10 @@ class GraphicsAPI extends BaseAPI {
return Math.max(...v);
}
approx(v1, v2, epsilon = 0.001) {
return Math.abs(v1 - v2) < epsilon;
}
sin(v) {
return Math.sin(v);
}
@@ -554,4 +668,4 @@ class GraphicsAPI extends BaseAPI {
}
}
export { GraphicsAPI, Bezier, Vector, Matrix };
export { GraphicsAPI, Bezier, Vector, Matrix, Shape };

View File

@@ -0,0 +1,111 @@
const HATCHING = [];
/**
* Build hatching patterns. These are built fully unrolled,
* mostly because they're small and there's no actual benefit
* to abstracting the drawing for only six patterns.
*/
function hatch(canvasBuildFunction) {
if (HATCHING.length > 0) {
return HATCHING;
}
let cvs,
ctx,
w = 9,
h = 9;
if (canvasBuildFunction) {
let b = canvasBuildFunction(w, h);
cvs = b.canvas;
ctx = b.ctx;
} else {
cvs = document.createElement("canvas");
cvs.width = w;
cvs.height = h;
ctx = cvs.getContext(`2d`);
}
ctx.fillStyle = `#0000FF30`;
ctx.lineWidth = 1;
const paint = (x, y) => ctx.fillRect(x, y, 1, 1);
// pattern: \
ctx.clearRect(0, 0, w, h);
paint(0, 0);
paint(1, 1);
paint(2, 2);
paint(3, 3);
paint(4, 4);
paint(5, 5);
paint(6, 6);
paint(7, 7);
paint(8, 8);
HATCHING.push(ctx.createPattern(cvs, "repeat"));
// pattern: /
ctx.clearRect(0, 0, w, h);
paint(0, 8);
paint(1, 7);
paint(2, 6);
paint(3, 5);
paint(4, 4);
paint(5, 3);
paint(6, 2);
paint(7, 1);
paint(8, 0);
HATCHING.push(ctx.createPattern(cvs, "repeat"));
// pattern: x (without clearing, because we can overlay)
paint(0, 0);
paint(1, 1);
paint(2, 2);
paint(3, 3);
paint(5, 5);
paint(6, 6);
paint(7, 7);
paint(8, 8);
HATCHING.push(ctx.createPattern(cvs, "repeat"));
// pattern: |
ctx.clearRect(0, 0, w, h);
paint(4, 0);
paint(4, 1);
paint(4, 2);
paint(4, 3);
paint(4, 4);
paint(4, 5);
paint(4, 6);
paint(4, 7);
paint(4, 8);
HATCHING.push(ctx.createPattern(cvs, "repeat"));
// pattern: -
ctx.clearRect(0, 0, w, h);
paint(0, 4);
paint(1, 4);
paint(2, 4);
paint(3, 4);
paint(4, 4);
paint(5, 4);
paint(6, 4);
paint(7, 4);
paint(8, 4);
HATCHING.push(ctx.createPattern(cvs, "repeat"));
// pattern: + (without clearing, because we can overlap)
paint(4, 0);
paint(4, 1);
paint(4, 2);
paint(4, 3);
paint(4, 5);
paint(4, 6);
paint(4, 7);
paint(4, 8);
HATCHING.push(ctx.createPattern(cvs, "repeat"));
return HATCHING;
}
export { hatch };

View File

@@ -4,10 +4,23 @@
* cubic Bezier curves.
*/
class Shape {
constructor(type, factor) {
constructor(type, factor, points = []) {
this.first = false;
this.segments = [];
this.addSegment(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;
}
addSegment(type, factor) {
this.currentSegment = new Segment(type, factor);
@@ -30,10 +43,15 @@ Shape.BEZIER = `Bezier`;
* A shape subpath
*/
class Segment {
constructor(type, factor) {
constructor(type, factor, points = []) {
this.type = type;
this.factor = factor;
this.points = [];
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);

View File

@@ -40,8 +40,8 @@ class GraphicsElement extends CustomElement {
return `
:host([hidden]) { display: none; }
:host style { display: none; }
:host canvas { position: relative; z-index: 1; display: block; margin: auto; border-radius: 0; box-sizing: content-box!important; padding: 1px; }
:host canvas:focus { border: 1px solid red; padding: 0px; }
: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 { display: block; position:relative; top: -0.6em; margin-bottom: -0.2em; font-size: 60%; text-decoration: none; }
:host label { display: block; font-style:italic; font-size: 0.9em; text-align: right; }
`;
@@ -171,7 +171,7 @@ class GraphicsElement extends CustomElement {
const height = this.getAttribute(`height`, 200);
this.code = `
import { GraphicsAPI, Bezier, Vector, Matrix } from "${MODULE_PATH}/api/graphics-api.js";
import { GraphicsAPI, Bezier, Vector, Matrix, Shape } from "${MODULE_PATH}/api/graphics-api.js";
${globalCode}

View File

@@ -0,0 +1,76 @@
/**
* This file takes any <img> with loading="lazy" and rewrites it
* so that it starts to load much earlier than browsers would load
* them, so that they're already done by the time the user gets to
* them. This prevents the ridiculous "don't start loading the img
* until we're already looking at the empty bounding box".
*/
const images = Array.from(
document.querySelectorAll(`img[loading=lazy]`)
).filter((img) => img.parentNode.nodeName !== `FALLBACK-IMAGE`);
// First, make images inert. As this happens before the document
// becomes active, this prevents images from loading anything.
//
// Also, by having JS deactivate the images, anyone running with
// JS disabled will still see images load in properly. In fact,
// the "lazy" attribute gets ignored when JS is disabled and all
// images will just load in immediately.
images.forEach((img) => {
// non-negotiable order of operations:
img.dataset.src = img.src;
img.src = ``;
img.removeAttribute(`loading`);
});
// Then tack on the functionality that reactivates them based on viewport distance.
let lock = false;
let retry = false;
/**
* Test all images during scroll, in a way that doesn't hang the browser.
*/
function testImages() {
if (lock) {
if (retry) retry = clearTimeout(retry);
retry = setTimeout(testImages, 200);
}
lock = true;
let top = window.scrollY;
let height = document.documentElement.clientHeight;
let bottom = top + height;
for (let pos = images.length - 1; pos >= 0; pos--) {
test(images[pos], pos, top, bottom, height);
}
if (images.length === 0) {
window.removeEventListener("scroll", testImages);
if (retry) clearTimeout(retry);
}
lock = false;
}
/**
* Test individual images for whether or not they should load.
*/
function test(img, pos, top, bottom, threshold) {
top = Math.abs(img.offsetTop - top) < threshold;
bottom = Math.abs(img.offsetTop + img.offsetHeight - bottom) < threshold;
if (top || bottom) {
img.src = img.dataset.src;
images.splice(pos, 1);
}
}
/**
* Remember to listen for scroll passively. If you don't, bad things happen.
*/
window.addEventListener("scroll", testImages, { passive: true });