You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
432 lines
15 KiB
JavaScript
432 lines
15 KiB
JavaScript
4 years ago
|
import { getFiller } from './fillers/filler.js';
|
||
|
import { Random } from './math.js';
|
||
|
import { parsePath, normalize, absolutize } from 'path-data-parser';
|
||
|
const helper = {
|
||
|
randOffset,
|
||
|
randOffsetWithRange,
|
||
|
ellipse,
|
||
|
doubleLineOps: doubleLineFillOps
|
||
|
};
|
||
|
export function line(x1, y1, x2, y2, o) {
|
||
|
return { type: 'path', ops: _doubleLine(x1, y1, x2, y2, o) };
|
||
|
}
|
||
|
export function linearPath(points, close, o) {
|
||
|
const len = (points || []).length;
|
||
|
if (len > 2) {
|
||
|
const ops = [];
|
||
|
for (let i = 0; i < (len - 1); i++) {
|
||
|
ops.push(..._doubleLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1], o));
|
||
|
}
|
||
|
if (close) {
|
||
|
ops.push(..._doubleLine(points[len - 1][0], points[len - 1][1], points[0][0], points[0][1], o));
|
||
|
}
|
||
|
return { type: 'path', ops };
|
||
|
}
|
||
|
else if (len === 2) {
|
||
|
return line(points[0][0], points[0][1], points[1][0], points[1][1], o);
|
||
|
}
|
||
|
return { type: 'path', ops: [] };
|
||
|
}
|
||
|
export function polygon(points, o) {
|
||
|
return linearPath(points, true, o);
|
||
|
}
|
||
|
export function rectangle(x, y, width, height, o) {
|
||
|
const points = [
|
||
|
[x, y],
|
||
|
[x + width, y],
|
||
|
[x + width, y + height],
|
||
|
[x, y + height]
|
||
|
];
|
||
|
return polygon(points, o);
|
||
|
}
|
||
|
export function curve(points, o) {
|
||
|
let o1 = _curveWithOffset(points, 1 * (1 + o.roughness * 0.2), o);
|
||
|
if (!o.disableMultiStroke) {
|
||
|
const o2 = _curveWithOffset(points, 1.5 * (1 + o.roughness * 0.22), cloneOptionsAlterSeed(o));
|
||
|
o1 = o1.concat(o2);
|
||
|
}
|
||
|
return { type: 'path', ops: o1 };
|
||
|
}
|
||
|
export function ellipse(x, y, width, height, o) {
|
||
|
const params = generateEllipseParams(width, height, o);
|
||
|
return ellipseWithParams(x, y, o, params).opset;
|
||
|
}
|
||
|
export function generateEllipseParams(width, height, o) {
|
||
|
const psq = Math.sqrt(Math.PI * 2 * Math.sqrt((Math.pow(width / 2, 2) + Math.pow(height / 2, 2)) / 2));
|
||
|
const stepCount = Math.max(o.curveStepCount, (o.curveStepCount / Math.sqrt(200)) * psq);
|
||
|
const increment = (Math.PI * 2) / stepCount;
|
||
|
let rx = Math.abs(width / 2);
|
||
|
let ry = Math.abs(height / 2);
|
||
|
const curveFitRandomness = 1 - o.curveFitting;
|
||
|
rx += _offsetOpt(rx * curveFitRandomness, o);
|
||
|
ry += _offsetOpt(ry * curveFitRandomness, o);
|
||
|
return { increment, rx, ry };
|
||
|
}
|
||
|
export function ellipseWithParams(x, y, o, ellipseParams) {
|
||
|
const [ap1, cp1] = _computeEllipsePoints(ellipseParams.increment, x, y, ellipseParams.rx, ellipseParams.ry, 1, ellipseParams.increment * _offset(0.1, _offset(0.4, 1, o), o), o);
|
||
|
let o1 = _curve(ap1, null, o);
|
||
|
if (!o.disableMultiStroke) {
|
||
|
const [ap2] = _computeEllipsePoints(ellipseParams.increment, x, y, ellipseParams.rx, ellipseParams.ry, 1.5, 0, o);
|
||
|
const o2 = _curve(ap2, null, o);
|
||
|
o1 = o1.concat(o2);
|
||
|
}
|
||
|
return {
|
||
|
estimatedPoints: cp1,
|
||
|
opset: { type: 'path', ops: o1 }
|
||
|
};
|
||
|
}
|
||
|
export function arc(x, y, width, height, start, stop, closed, roughClosure, o) {
|
||
|
const cx = x;
|
||
|
const cy = y;
|
||
|
let rx = Math.abs(width / 2);
|
||
|
let ry = Math.abs(height / 2);
|
||
|
rx += _offsetOpt(rx * 0.01, o);
|
||
|
ry += _offsetOpt(ry * 0.01, o);
|
||
|
let strt = start;
|
||
|
let stp = stop;
|
||
|
while (strt < 0) {
|
||
|
strt += Math.PI * 2;
|
||
|
stp += Math.PI * 2;
|
||
|
}
|
||
|
if ((stp - strt) > (Math.PI * 2)) {
|
||
|
strt = 0;
|
||
|
stp = Math.PI * 2;
|
||
|
}
|
||
|
const ellipseInc = (Math.PI * 2) / o.curveStepCount;
|
||
|
const arcInc = Math.min(ellipseInc / 2, (stp - strt) / 2);
|
||
|
const ops = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1, o);
|
||
|
if (!o.disableMultiStroke) {
|
||
|
const o2 = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1.5, o);
|
||
|
ops.push(...o2);
|
||
|
}
|
||
|
if (closed) {
|
||
|
if (roughClosure) {
|
||
|
ops.push(..._doubleLine(cx, cy, cx + rx * Math.cos(strt), cy + ry * Math.sin(strt), o), ..._doubleLine(cx, cy, cx + rx * Math.cos(stp), cy + ry * Math.sin(stp), o));
|
||
|
}
|
||
|
else {
|
||
|
ops.push({ op: 'lineTo', data: [cx, cy] }, { op: 'lineTo', data: [cx + rx * Math.cos(strt), cy + ry * Math.sin(strt)] });
|
||
|
}
|
||
|
}
|
||
|
return { type: 'path', ops };
|
||
|
}
|
||
|
export function svgPath(path, o) {
|
||
|
const segments = normalize(absolutize(parsePath(path)));
|
||
|
const ops = [];
|
||
|
let first = [0, 0];
|
||
|
let current = [0, 0];
|
||
|
for (const { key, data } of segments) {
|
||
|
switch (key) {
|
||
|
case 'M': {
|
||
|
const ro = 1 * (o.maxRandomnessOffset || 0);
|
||
|
ops.push({ op: 'move', data: data.map((d) => d + _offsetOpt(ro, o)) });
|
||
|
current = [data[0], data[1]];
|
||
|
first = [data[0], data[1]];
|
||
|
break;
|
||
|
}
|
||
|
case 'L':
|
||
|
ops.push(..._doubleLine(current[0], current[1], data[0], data[1], o));
|
||
|
current = [data[0], data[1]];
|
||
|
break;
|
||
|
case 'C': {
|
||
|
const [x1, y1, x2, y2, x, y] = data;
|
||
|
ops.push(..._bezierTo(x1, y1, x2, y2, x, y, current, o));
|
||
|
current = [x, y];
|
||
|
break;
|
||
|
}
|
||
|
case 'Z':
|
||
|
ops.push(..._doubleLine(current[0], current[1], first[0], first[1], o));
|
||
|
current = [first[0], first[1]];
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
return { type: 'path', ops };
|
||
|
}
|
||
|
// Fills
|
||
|
export function solidFillPolygon(points, o) {
|
||
|
const ops = [];
|
||
|
if (points.length) {
|
||
|
const offset = o.maxRandomnessOffset || 0;
|
||
|
const len = points.length;
|
||
|
if (len > 2) {
|
||
|
ops.push({ op: 'move', data: [points[0][0] + _offsetOpt(offset, o), points[0][1] + _offsetOpt(offset, o)] });
|
||
|
for (let i = 1; i < len; i++) {
|
||
|
ops.push({ op: 'lineTo', data: [points[i][0] + _offsetOpt(offset, o), points[i][1] + _offsetOpt(offset, o)] });
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return { type: 'fillPath', ops };
|
||
|
}
|
||
|
export function patternFillPolygon(points, o) {
|
||
|
return getFiller(o, helper).fillPolygon(points, o);
|
||
|
}
|
||
|
export function patternFillArc(x, y, width, height, start, stop, o) {
|
||
|
const cx = x;
|
||
|
const cy = y;
|
||
|
let rx = Math.abs(width / 2);
|
||
|
let ry = Math.abs(height / 2);
|
||
|
rx += _offsetOpt(rx * 0.01, o);
|
||
|
ry += _offsetOpt(ry * 0.01, o);
|
||
|
let strt = start;
|
||
|
let stp = stop;
|
||
|
while (strt < 0) {
|
||
|
strt += Math.PI * 2;
|
||
|
stp += Math.PI * 2;
|
||
|
}
|
||
|
if ((stp - strt) > (Math.PI * 2)) {
|
||
|
strt = 0;
|
||
|
stp = Math.PI * 2;
|
||
|
}
|
||
|
const increment = (stp - strt) / o.curveStepCount;
|
||
|
const points = [];
|
||
|
for (let angle = strt; angle <= stp; angle = angle + increment) {
|
||
|
points.push([cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)]);
|
||
|
}
|
||
|
points.push([cx + rx * Math.cos(stp), cy + ry * Math.sin(stp)]);
|
||
|
points.push([cx, cy]);
|
||
|
return patternFillPolygon(points, o);
|
||
|
}
|
||
|
export function randOffset(x, o) {
|
||
|
return _offsetOpt(x, o);
|
||
|
}
|
||
|
export function randOffsetWithRange(min, max, o) {
|
||
|
return _offset(min, max, o);
|
||
|
}
|
||
|
export function doubleLineFillOps(x1, y1, x2, y2, o) {
|
||
|
return _doubleLine(x1, y1, x2, y2, o, true);
|
||
|
}
|
||
|
// Private helpers
|
||
|
function cloneOptionsAlterSeed(ops) {
|
||
|
const result = Object.assign({}, ops);
|
||
|
result.randomizer = undefined;
|
||
|
if (ops.seed) {
|
||
|
result.seed = ops.seed + 1;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
function random(ops) {
|
||
|
if (!ops.randomizer) {
|
||
|
ops.randomizer = new Random(ops.seed || 0);
|
||
|
}
|
||
|
return ops.randomizer.next();
|
||
|
}
|
||
|
function _offset(min, max, ops, roughnessGain = 1) {
|
||
|
return ops.roughness * roughnessGain * ((random(ops) * (max - min)) + min);
|
||
|
}
|
||
|
function _offsetOpt(x, ops, roughnessGain = 1) {
|
||
|
return _offset(-x, x, ops, roughnessGain);
|
||
|
}
|
||
|
function _doubleLine(x1, y1, x2, y2, o, filling = false) {
|
||
|
const singleStroke = filling ? o.disableMultiStrokeFill : o.disableMultiStroke;
|
||
|
const o1 = _line(x1, y1, x2, y2, o, true, false);
|
||
|
if (singleStroke) {
|
||
|
return o1;
|
||
|
}
|
||
|
const o2 = _line(x1, y1, x2, y2, o, true, true);
|
||
|
return o1.concat(o2);
|
||
|
}
|
||
|
function _line(x1, y1, x2, y2, o, move, overlay) {
|
||
|
const lengthSq = Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2);
|
||
|
const length = Math.sqrt(lengthSq);
|
||
|
let roughnessGain = 1;
|
||
|
if (length < 200) {
|
||
|
roughnessGain = 1;
|
||
|
}
|
||
|
else if (length > 500) {
|
||
|
roughnessGain = 0.4;
|
||
|
}
|
||
|
else {
|
||
|
roughnessGain = (-0.0016668) * length + 1.233334;
|
||
|
}
|
||
|
let offset = o.maxRandomnessOffset || 0;
|
||
|
if ((offset * offset * 100) > lengthSq) {
|
||
|
offset = length / 10;
|
||
|
}
|
||
|
const halfOffset = offset / 2;
|
||
|
const divergePoint = 0.2 + random(o) * 0.2;
|
||
|
let midDispX = o.bowing * o.maxRandomnessOffset * (y2 - y1) / 200;
|
||
|
let midDispY = o.bowing * o.maxRandomnessOffset * (x1 - x2) / 200;
|
||
|
midDispX = _offsetOpt(midDispX, o, roughnessGain);
|
||
|
midDispY = _offsetOpt(midDispY, o, roughnessGain);
|
||
|
const ops = [];
|
||
|
const randomHalf = () => _offsetOpt(halfOffset, o, roughnessGain);
|
||
|
const randomFull = () => _offsetOpt(offset, o, roughnessGain);
|
||
|
if (move) {
|
||
|
if (overlay) {
|
||
|
ops.push({
|
||
|
op: 'move', data: [
|
||
|
x1 + randomHalf(),
|
||
|
y1 + randomHalf()
|
||
|
]
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
ops.push({
|
||
|
op: 'move', data: [
|
||
|
x1 + _offsetOpt(offset, o, roughnessGain),
|
||
|
y1 + _offsetOpt(offset, o, roughnessGain)
|
||
|
]
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
if (overlay) {
|
||
|
ops.push({
|
||
|
op: 'bcurveTo', data: [
|
||
|
midDispX + x1 + (x2 - x1) * divergePoint + randomHalf(),
|
||
|
midDispY + y1 + (y2 - y1) * divergePoint + randomHalf(),
|
||
|
midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomHalf(),
|
||
|
midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomHalf(),
|
||
|
x2 + randomHalf(),
|
||
|
y2 + randomHalf()
|
||
|
]
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
ops.push({
|
||
|
op: 'bcurveTo', data: [
|
||
|
midDispX + x1 + (x2 - x1) * divergePoint + randomFull(),
|
||
|
midDispY + y1 + (y2 - y1) * divergePoint + randomFull(),
|
||
|
midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomFull(),
|
||
|
midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomFull(),
|
||
|
x2 + randomFull(),
|
||
|
y2 + randomFull()
|
||
|
]
|
||
|
});
|
||
|
}
|
||
|
return ops;
|
||
|
}
|
||
|
function _curveWithOffset(points, offset, o) {
|
||
|
const ps = [];
|
||
|
ps.push([
|
||
|
points[0][0] + _offsetOpt(offset, o),
|
||
|
points[0][1] + _offsetOpt(offset, o),
|
||
|
]);
|
||
|
ps.push([
|
||
|
points[0][0] + _offsetOpt(offset, o),
|
||
|
points[0][1] + _offsetOpt(offset, o),
|
||
|
]);
|
||
|
for (let i = 1; i < points.length; i++) {
|
||
|
ps.push([
|
||
|
points[i][0] + _offsetOpt(offset, o),
|
||
|
points[i][1] + _offsetOpt(offset, o),
|
||
|
]);
|
||
|
if (i === (points.length - 1)) {
|
||
|
ps.push([
|
||
|
points[i][0] + _offsetOpt(offset, o),
|
||
|
points[i][1] + _offsetOpt(offset, o),
|
||
|
]);
|
||
|
}
|
||
|
}
|
||
|
return _curve(ps, null, o);
|
||
|
}
|
||
|
function _curve(points, closePoint, o) {
|
||
|
const len = points.length;
|
||
|
const 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) {
|
||
|
const ro = o.maxRandomnessOffset;
|
||
|
ops.push({ op: 'lineTo', data: [closePoint[0] + _offsetOpt(ro, o), closePoint[1] + _offsetOpt(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) {
|
||
|
ops.push(..._doubleLine(points[0][0], points[0][1], points[1][0], points[1][1], o));
|
||
|
}
|
||
|
return ops;
|
||
|
}
|
||
|
function _computeEllipsePoints(increment, cx, cy, rx, ry, offset, overlap, o) {
|
||
|
const corePoints = [];
|
||
|
const allPoints = [];
|
||
|
const radOffset = _offsetOpt(0.5, o) - (Math.PI / 2);
|
||
|
allPoints.push([
|
||
|
_offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment),
|
||
|
_offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment)
|
||
|
]);
|
||
|
for (let angle = radOffset; angle < (Math.PI * 2 + radOffset - 0.01); angle = angle + increment) {
|
||
|
const p = [
|
||
|
_offsetOpt(offset, o) + cx + rx * Math.cos(angle),
|
||
|
_offsetOpt(offset, o) + cy + ry * Math.sin(angle)
|
||
|
];
|
||
|
corePoints.push(p);
|
||
|
allPoints.push(p);
|
||
|
}
|
||
|
allPoints.push([
|
||
|
_offsetOpt(offset, o) + cx + rx * Math.cos(radOffset + Math.PI * 2 + overlap * 0.5),
|
||
|
_offsetOpt(offset, o) + cy + ry * Math.sin(radOffset + Math.PI * 2 + overlap * 0.5)
|
||
|
]);
|
||
|
allPoints.push([
|
||
|
_offsetOpt(offset, o) + cx + 0.98 * rx * Math.cos(radOffset + overlap),
|
||
|
_offsetOpt(offset, o) + cy + 0.98 * ry * Math.sin(radOffset + overlap)
|
||
|
]);
|
||
|
allPoints.push([
|
||
|
_offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset + overlap * 0.5),
|
||
|
_offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset + overlap * 0.5)
|
||
|
]);
|
||
|
return [allPoints, corePoints];
|
||
|
}
|
||
|
function _arc(increment, cx, cy, rx, ry, strt, stp, offset, o) {
|
||
|
const radOffset = strt + _offsetOpt(0.1, o);
|
||
|
const points = [];
|
||
|
points.push([
|
||
|
_offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment),
|
||
|
_offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment)
|
||
|
]);
|
||
|
for (let angle = radOffset; angle <= stp; angle = angle + increment) {
|
||
|
points.push([
|
||
|
_offsetOpt(offset, o) + cx + rx * Math.cos(angle),
|
||
|
_offsetOpt(offset, o) + cy + ry * Math.sin(angle)
|
||
|
]);
|
||
|
}
|
||
|
points.push([
|
||
|
cx + rx * Math.cos(stp),
|
||
|
cy + ry * Math.sin(stp)
|
||
|
]);
|
||
|
points.push([
|
||
|
cx + rx * Math.cos(stp),
|
||
|
cy + ry * Math.sin(stp)
|
||
|
]);
|
||
|
return _curve(points, null, o);
|
||
|
}
|
||
|
function _bezierTo(x1, y1, x2, y2, x, y, current, o) {
|
||
|
const ops = [];
|
||
|
const ros = [o.maxRandomnessOffset || 1, (o.maxRandomnessOffset || 1) + 0.3];
|
||
|
let f = [0, 0];
|
||
|
const iterations = o.disableMultiStroke ? 1 : 2;
|
||
|
for (let i = 0; i < iterations; i++) {
|
||
|
if (i === 0) {
|
||
|
ops.push({ op: 'move', data: [current[0], current[1]] });
|
||
|
}
|
||
|
else {
|
||
|
ops.push({ op: 'move', data: [current[0] + _offsetOpt(ros[0], o), current[1] + _offsetOpt(ros[0], o)] });
|
||
|
}
|
||
|
f = [x + _offsetOpt(ros[i], o), y + _offsetOpt(ros[i], o)];
|
||
|
ops.push({
|
||
|
op: 'bcurveTo', data: [
|
||
|
x1 + _offsetOpt(ros[i], o), y1 + _offsetOpt(ros[i], o),
|
||
|
x2 + _offsetOpt(ros[i], o), y2 + _offsetOpt(ros[i], o),
|
||
|
f[0], f[1]
|
||
|
]
|
||
|
});
|
||
|
}
|
||
|
return ops;
|
||
|
}
|