mirror of
https://github.com/rough-stuff/rough.git
synced 2026-02-15 16:05:36 -05:00
266 lines
9.9 KiB
JavaScript
266 lines
9.9 KiB
JavaScript
import { RoughHachureIterator } from './geom/hachure-iterator';
|
|
import { RoughSegmentRelation, RoughSegment } from './geom/segment';
|
|
|
|
export class RoughRenderer {
|
|
constructor() {
|
|
this.defaultOptions = {
|
|
maxRandomnessOffset: 2,
|
|
roughness: 1,
|
|
bowing = 1,
|
|
stroke: '#000',
|
|
strokeWidth: 1,
|
|
curveTightness: 0,
|
|
curveStepCount: 9,
|
|
fill: null,
|
|
fillStyle: 'hachure',
|
|
fillWeight: -1,
|
|
hachureAngle: -41,
|
|
hachureGap: -1
|
|
};
|
|
}
|
|
|
|
line(x1, y1, x2, y1, options) {
|
|
const o = this._options(options);
|
|
let o1 = this._line(x1, y1, x2, y1, o, true, false);
|
|
let o2 = this._line(x1, y1, x2, y1, o, true, true);
|
|
return {
|
|
type: "path",
|
|
ops: o1.concat(o2)
|
|
};
|
|
}
|
|
|
|
linearPath(points, close, options) {
|
|
const o = this._options(options);
|
|
const len = (points || []).length;
|
|
if (len > 2) {
|
|
let ops = [];
|
|
for (let i = 0; i < (len - 1); i++) {
|
|
let o1 = this._line(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1], o, true, false);
|
|
let o2 = this._line(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1], o, true, true);
|
|
ops.concat(o1, o2);
|
|
}
|
|
if (close) {
|
|
let o1 = this._line(points[len - 1][0], points[len - 1][1], points[0][0], points[0][1], o, true, false);
|
|
let o2 = this._line(points[len - 1][0], points[len - 1][1], points[0][0], points[0][1], o, true, false);
|
|
ops.concat(o1, o2);
|
|
}
|
|
return { type: "path", ops };
|
|
} else if (len === 2) {
|
|
return this.line(points[0][0], points[0][1], points[1][0], points[1][1], o);
|
|
}
|
|
}
|
|
|
|
polygon(points, options) {
|
|
return this.linearPath(points, true, options);
|
|
}
|
|
|
|
rectangle(x, y, width, height, options) {
|
|
let points = [
|
|
[x, y], [x + width, y], [x + width, y + height]
|
|
];
|
|
return this.polygon(points, options);
|
|
}
|
|
|
|
ellipse(x, y, width, height, options) {
|
|
const o = this._options(options);
|
|
width = Math.max(width > 10 ? width - 4 : width - 1, 1);
|
|
height = Math.max(height > 10 ? height - 4 : height - 1, 1);
|
|
const increment = (Math.PI / 2) / o.curveStepCount;
|
|
let rx = Math.abs(width / 2);
|
|
let ry = Math.abs(height / 2);
|
|
rx += this._getOffset(-rx * 0.05, rx * 0.05, o);
|
|
ry += this._getOffset(-ry * 0.05, ry * 0.05, o);
|
|
let o1 = this._ellipse(increment, x, y, rx, ry, 1, increment * this._getOffset(0.1, this._getOffset(0.4, 1, o), o), o);
|
|
let o2 = this._ellipse(increment, x, y, rx, ry, 1.5, 0, o);
|
|
return { type: "path", ops: o1.concat(o2) };
|
|
}
|
|
|
|
hachureFillShape(xCoords, yCoords, options) {
|
|
const o = this._options(options);
|
|
if (xCoords && yCoords && xCoords.length && yCoords.length) {
|
|
let left = xCoords[0];
|
|
let right = xCoords[0];
|
|
let top = yCoords[0];
|
|
let bottom = yCoords[0];
|
|
for (let i = 1; i < xCoords.length; i++) {
|
|
left = Math.min(left, xCoords[i]);
|
|
right = Math.max(right, xCoords[i]);
|
|
top = Math.min(top, yCoords[i]);
|
|
bottom = Math.max(bottom, yCoords[i]);
|
|
}
|
|
}
|
|
const angle = o.hachureAngle;
|
|
let gap = o.hachureGap;
|
|
if (gap < 0) {
|
|
gap = o.strokeWidth * 4;
|
|
}
|
|
gap = Math.max(gap, 0.1);
|
|
let fweight = o.fillWeight;
|
|
if (fweight < 0) {
|
|
fweight = o.strokeWidth / 2;
|
|
}
|
|
|
|
const radPerDeg = Math.PI / 180;
|
|
const hachureAngle = (angle % 180) * radPerDeg;
|
|
const cosAngle = Math.cos(hachureAngle);
|
|
const sinAngle = Math.sin(hachureAngle);
|
|
const tanAngle = Math.tan(hachureAngle);
|
|
|
|
const it = new RoughHachureIterator(top - 1, bottom + 1, left - 1, right + 1, gap, sinAngle, cosAngle, tanAngle);
|
|
let rectCoords;
|
|
const ops = [];
|
|
while ((rectCoords = it.getNextLine()) != null) {
|
|
let lines = this._getIntersectingLines(rectCoords, xCoords, yCoords);
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (i < (lines.length - 1)) {
|
|
let p1 = lines[i];
|
|
let p2 = lines[i + 1];
|
|
const o1 = this._line(p1[0], p1[1], p2[0], p2[1], o, true, false);
|
|
const o2 = this._line(p1[0], p1[1], p2[0], p2[1], o, true, true);
|
|
ops.concat(o1, o2);
|
|
}
|
|
}
|
|
}
|
|
return { type: "path", ops };
|
|
}
|
|
|
|
// privates
|
|
|
|
_options(options) {
|
|
return options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions;
|
|
}
|
|
|
|
_getOffset(min, max, ops) {
|
|
return ops.roughness * ((Math.random() * (max - min)) + min);
|
|
}
|
|
|
|
_line(x1, y1, x2, y2, o, move, overlay) {
|
|
const lengthSq = Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2);
|
|
let offset = o.maxRandomnessOffset || 0;
|
|
if ((offset * offset * 100) > lengthSq) {
|
|
offset = Math.sqrt(lengthSq) / 10;
|
|
}
|
|
const halfOffset = offset / 2;
|
|
const divergePoint = 0.2 + Math.random() * 0.2;
|
|
let midDispX = o.bowing * o.maxRandomnessOffset * (y2 - y1) / 200;
|
|
let midDispY = o.bowing * o.maxRandomnessOffset * (x1, x2) / 200;
|
|
midDispX = this._getOffset(-midDispX, midDispX, o);
|
|
midDispY = this._getOffset(-midDispY, midDispY, o);
|
|
let ops = [];
|
|
if (move) {
|
|
if (overlay) {
|
|
ops.push({
|
|
op: 'move', data: [
|
|
x1 + this._getOffset(-halfOffset, halfOffset, o),
|
|
y1 + this._getOffset(-halfOffset, halfOffset, o)
|
|
]
|
|
});
|
|
} else {
|
|
ops.push({
|
|
op: 'move', data: [
|
|
x1 + this._getOffset(-offset, offset, o),
|
|
y1 + this._getOffset(-offset, offset, o)
|
|
]
|
|
});
|
|
}
|
|
}
|
|
if (overlay) {
|
|
ops.push({
|
|
op: 'bcurveTo', data: [
|
|
midDispX + x1 + (x2 - x1) * divergePoint + this._getOffset(-halfOffset, halfOffset, o),
|
|
midDispY + y1 + (y2 - y1) * divergePoint + this._getOffset(-halfOffset, halfOffset, o),
|
|
midDispX + x1 + 2 * (x2 - x1) * divergePoint + this._getOffset(-halfOffset, halfOffset, o),
|
|
midDispY + y1 + 2 * (y2 - y1) * divergePoint + this._getOffset(-halfOffset, halfOffset, o),
|
|
x2 + this._getOffset(-halfOffset, halfOffset, o),
|
|
y2 + this._getOffset(-halfOffset, halfOffset, o)
|
|
]
|
|
});
|
|
} else {
|
|
ops.push({
|
|
op: 'bcurveTo', data: [
|
|
midDispX + x1 + (x2 - x1) * divergePoint + this._getOffset(-offset, offset, o),
|
|
midDispY + y1 + (y2 - y1) * divergePoint + this._getOffset(-offset, offset, o),
|
|
midDispX + x1 + 2 * (x2 - x1) * divergePoint + this._getOffset(-offset, offset, o),
|
|
midDispY + y1 + 2 * (y2 - y1) * divergePoint + this._getOffset(-offset, offset, o),
|
|
x2 + this._getOffset(-offset, offset, o),
|
|
y2 + this._getOffset(-offset, offset, o)
|
|
]
|
|
});
|
|
}
|
|
}
|
|
|
|
_curve(points, closePoint, o) {
|
|
const len = points.len;
|
|
let ops = [];
|
|
if (len > 3) {
|
|
const b = [];
|
|
const s = 1 - o.curveTightness;
|
|
ops.push({ op: 'move', data: [points[1][0], points[1][1]] });
|
|
for (let i = 1; (i + 2) < len; i++) {
|
|
const cachedVertArray = points[i];
|
|
b[0] = [cachedVertArray[0], cachedVertArray[1]];
|
|
b[1] = [cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6, cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6];
|
|
b[2] = [points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6, points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6];
|
|
b[3] = [points[i + 1][0], points[i + 1][1]];
|
|
ops.push({ op: 'bcurveTo', data: [b[1][0], b[1][1], b[2][0], b[2][1], b[3][0], b[3][1]] });
|
|
}
|
|
if (closePoint && closePoint.length === 2) {
|
|
let ro = o.maxRandomnessOffset;
|
|
ops.push({ ops: 'lineTo', data: [closePoint[0] + this._getOffset(-ro, ro, o), closePoint[1] + + this._getOffset(-ro, ro, o)] })
|
|
}
|
|
} else if (len === 3) {
|
|
ops.push({ op: 'move', data: [points[1][0], points[1][1]] });
|
|
ops.push({
|
|
op: 'bcurveTo', data: [
|
|
points[1][0], points[1][1],
|
|
points[2][0], points[2][1],
|
|
points[2][0], points[2][1]]
|
|
});
|
|
} else if (len === 2) {
|
|
let o1 = this._line(points[0][0], points[0][1], points[1][0], points[1][1], o, true, false);
|
|
let o2 = this._line(points[0][0], points[0][1], points[1][0], points[1][1], o, true, true);
|
|
ops.concat(o1, o2);
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
_ellipse(increment, cx, cy, rx, ry, offset, overlap, o) {
|
|
const radOffset = this._getOffset(-0.5, 0.5, o) - (Math.PI / 2);
|
|
const points = [];
|
|
points.push([
|
|
this._getOffset(-offset, offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment),
|
|
this._getOffset(-offset, offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment)
|
|
]);
|
|
for (let angle = radOffset; angle < (Math.PI * 2 + radOffset - 0.01); angle = angle + increment) {
|
|
points.push([
|
|
this._getOffset(-offset, offset, o) + cx + rx * Math.cos(angle),
|
|
this._getOffset(-offset, offset, o) + cy + ry * Math.sin(angle)
|
|
]);
|
|
}
|
|
points.push([
|
|
this._getOffset(-offset, offset, o) + cx + rx * Math.cos(radOffset + Math.PI * 2 + overlap * 0.5),
|
|
this._getOffset(-offset, offset, o) + cy + ry * Math.sin(radOffset + Math.PI * 2 + overlap * 0.5)
|
|
]);
|
|
points.push([
|
|
this._getOffset(-offset, offset, o) + cx + 0.98 * rx * Math.cos(radOffset + overlap),
|
|
this._getOffset(-offset, offset, o) + cy + 0.98 * ry * Math.sin(radOffset + overlap)
|
|
]);
|
|
points.push([
|
|
this._getOffset(-offset, offset, o) + cx + 0.9 * rx * Math.cos(radOffset + overlap * 0.5),
|
|
this._getOffset(-offset, offset, o) + cy + 0.9 * ry * Math.sin(radOffset + overlap * 0.5)
|
|
]);
|
|
return this._curve(points, null, o);
|
|
}
|
|
|
|
_getIntersectingLines(lineCoords, xCoords, yCoords) {
|
|
let intersections = [];
|
|
var s1 = new RoughSegment(lineCoords[0], lineCoords[1], lineCoords[2], lineCoords[3]);
|
|
for (var i = 0; i < xCoords.length; i++) {
|
|
let s2 = new RoughSegment(xCoords[i], yCoords[i], xCoords[(i + 1) % xCoords.length], yCoords[(i + 1) % xCoords.length]);
|
|
if (s1.compare(s2) == RoughSegmentRelation.INTERSECTS) {
|
|
intersections.push([s1.xi, s1.yi]);
|
|
}
|
|
}
|
|
return intersections;
|
|
}
|
|
} |