perfect-freehand
Advanced tools
Comparing version 0.5.4 to 1.0.0
@@ -1,1 +0,1 @@ | ||
var me=Object.defineProperty;var ke=e=>me(e,"__esModule",{value:!0});var Se=(e,t)=>{ke(e);for(var n in t)me(e,n,{get:t[n],enumerable:!0})};Se(exports,{default:()=>Me,getStroke:()=>xe,getStrokeOutlinePoints:()=>he,getStrokePoints:()=>fe});function d(e,t){return[e[0]+t[0],e[1]+t[1]]}function h(e,t){return[e[0]-t[0],e[1]-t[1]]}function U(e,t){return[t[0]-e[0],t[1]-e[1]]}function i(e,t){return[e[0]*t,e[1]*t]}function Pe(e,t){return[e[0]/t,e[1]/t]}function y(e){return[e[1],-e[0]]}function le(e,t){return e[0]*t[0]+e[1]*t[1]}function ye(e){return Math.hypot(e[0],e[1])}function Ee(e){return e[0]*e[0]+e[1]*e[1]}function te(e,t){return Ee(h(e,t))}function D(e){return Pe(e,ye(e))}function F(e,t){return Math.hypot(e[1]-t[1],e[0]-t[0])}function ae(e,t){return i(d(e,t),.5)}function G(e,t,n){let c=Math.sin(n),R=Math.cos(n),p=e[0]-t[0],k=e[1]-t[1],v=p*R-k*c,w=p*c+k*R;return[v+t[0],w+t[1]]}function H(e,t,n){return d(e,i(U(e,t),n))}function W(e,t){return e[0]===t[0]&&e[1]===t[1]}function be(e,t,n){return e*(1-n)+t*n}function ne(e,t,n){return Math.max(t,Math.min(n,e))}function ve(e){return Array.isArray(e[0])?e.map(([t,n,c=.5])=>[t,n,c]):e.map(({x:t,y:n,pressure:c=.5})=>[t,n,c])}function $(e,t,n,c=.5){return t?(c=ne(n(c),0,1),(t<0?be(e,e+e*ne(t,-.95,-.05),c):be(e-e*ne(t,.05,.95),e,c))/2):e/2}var{min:I,PI:X}=Math;function fe(e,t={}){let{streamline:n=.5}=t,{simulatePressure:c=!0,last:R=!1}=t;if(e.length===0)return[];n=n/(c?3:2);let p=ve(e);p.length===1&&p.push([...d(p[0],[1,1]),p[0][2]]);let k=[],v={point:[p[0][0],p[0][1]],pressure:p[0][2],vector:[0,0],distance:0,runningLength:0};k.push(v);let w=p.length,T;for(let E=0;E<w;E++){T=p[E];let V=R&&E===w-1?T:H(v.point,T,1-n);if(W(v.point,V))continue;let M=D(h(v.point,V)),Y=F(V,v.point),A=v.runningLength+Y;v={point:V,pressure:T[2],vector:M,distance:Y,runningLength:A},k.push(v)}return k}var de=.3;function he(e,t={}){let{size:n=8,thinning:c=.5,smoothing:R=.5,simulatePressure:p=!0,easing:k=r=>r,start:v={},end:w={},last:T=!1}=t,{streamline:E=.5}=t;E/=2;let{cap:V=!0,taper:M=0,easing:Y=r=>r*(2-r)}=v,{cap:A=!0,taper:C=0,easing:ge=r=>--r*r*r+1}=w,K=e.length;if(K===0)return[];let re=e[K-1].runningLength,f=[],S=[],B=e.slice(0,10).reduce((r,g)=>{let a=g.pressure;if(p){let o=I(1,g.distance/n),b=I(1,1-o);a=I(1,r+(b-r)*(o*de))}return(r+a)/2},e[0].pressure),P=$(n,c,k,e[K-1].pressure),ue,oe=e[0].vector,_=e[0].point,N=_,m=_,l=N,Z=!0;for(let r=0;r<K-1;r++){let{pressure:g}=e[r],{point:a,vector:o,distance:b,runningLength:u}=e[r];if(r>0&&Z&&u<n/2)continue;if(Z&&(Z=!1),c){if(p){let Q=I(1,b/n),j=I(1,1-Q);g=I(1,B+(j-B)*(Q*de))}P=$(n,c,k,g)}else P=n/2;ue===void 0&&(ue=P);let s=u<M?Y(u/M):1,J=re-u<C?ge((re-u)/C):1;P=Math.max(.01,P*Math.min(s,J));let O=e[r+1].vector,ee=le(o,O);if(ee<0){let Q=i(y(oe),P);for(let j=0;j<1;j+=.2)l=G(d(a,Q),a,X*-j),m=G(h(a,Q),a,X*j),S.push(l),f.push(m);_=m,N=l;continue}let ce=i(y(H(O,o,ee)),P);m=h(a,ce),l=d(a,ce);let ie=r<2||ee<.25,pe=Math.pow(Math.max((u>n?n:n/2)*R,1),2);(ie||te(_,m)>pe)&&(f.push(H(_,m,E)),_=m),(ie||te(N,l)>pe)&&(S.push(H(N,l,E)),N=l),B=g,oe=o}let x=e[0],se=e[K-1],z=Z||S.length<2||f.length<2;if(z&&(!(M||C)||T)){let r=0;for(let o=0;o<K;o++){let{pressure:b,runningLength:u}=e[o];if(u>n){r=$(n,c,k,b);break}}let g=h(x.point,i(y(D(U(se.point,x.point))),r||P)),a=[];for(let o=0,b=.1;o<=1;o+=b)a.push(G(g,x.point,X*2*o));return a}let L=[],q=[];if(f.length>1&&S.length>1){l=S[1];for(let u=1;u<f.length;u++)if(!W(l,f[u])){m=f[u];break}if(V||M)if(!M&&!(C&&z)){if(!W(l,m)){let u=h(x.point,i(D(U(l,m)),F(l,m)/2));for(let s=0,J=.1;s<=1;s+=J){let O=G(u,x.point,X*s);if(F(O,m)<1)break;L.push(O)}f.shift(),S.shift()}}else L.push(x.point,d(x.point,[.1,.1]));else if(!W(l,m)){let u=D(U(l,m)),s=F(l,m)/2;L.push(h(x.point,i(u,s*.95))),L.push(h(x.point,i(u,s))),L.push(d(x.point,i(u,s))),L.push(d(x.point,i(u,s*.95))),f.shift(),S.shift()}let r=f[f.length-1],g=S[S.length-1],a=ae(r,g),o=se.point,b=D(h(o,a));if(A||C)if(!C&&!(M&&z)){let u=d(o,i(y(b),P));for(let s=0,J=.1;s<=1;s+=J){let O=G(u,o,X*3*s);if(F(O,g)<1)break;q.push(O)}}else q.push(o);else{let u=H(a,o,.95),s=P*.95;q.push(d(u,i(y(b),s))),q.push(d(o,i(y(b),s))),q.push(h(o,i(y(b),s))),q.push(h(u,i(y(b),s)))}}return f.concat(q,S.reverse(),L)}function xe(e,t={}){return he(fe(e,t),t)}var Me=xe; | ||
var at=Object.defineProperty;var ht=t=>at(t,"__esModule",{value:!0});var dt=(t,e)=>{ht(t);for(var n in e)at(t,n,{get:e[n],enumerable:!0})};dt(exports,{default:()=>Pt,getStroke:()=>rt,getStrokeOutlinePoints:()=>et,getStrokePoints:()=>nt});function Y(t,e,n,E=g=>g){return t*E(.5-e*(.5-n))}function v(t,e){return[t[0]+e[0],t[1]+e[1]]}function x(t,e){return[t[0]-e[0],t[1]-e[1]]}function z(t,e){return[t[0]*e,t[1]*e]}function xt(t,e){return[t[0]/e,t[1]/e]}function N(t){return[t[1],-t[0]]}function lt(t,e){return t[0]*e[0]+t[1]*e[1]}function C(t,e){return t[0]===e[0]&&t[1]===e[1]}function kt(t){return Math.hypot(t[0],t[1])}function St(t){return t[0]*t[0]+t[1]*t[1]}function tt(t,e){return St(x(t,e))}function T(t){return xt(t,kt(t))}function K(t,e){return Math.hypot(t[1]-e[1],t[0]-e[0])}function ft(t,e){return z(v(t,e),.5)}function _(t,e,n){let E=Math.sin(n),g=Math.cos(n),i=t[0]-e[0],k=t[1]-e[1],l=i*g-k*E,j=i*E+k*g;return[l+e[0],j+e[1]]}function R(t,e,n){return v(t,z(x(e,t),n))}function b(t,e,n){return v(t,z(e,n))}var bt=.3,{min:F,PI:J}=Math;function et(t,e={}){let{size:n=16,smoothing:E=.5,thinning:g=.5,simulatePressure:i=!0,easing:k=r=>r,start:l={},end:j={},last:q=!1}=e,{streamline:S=.5}=e,{cap:M=!0,taper:P=0,easing:Q=r=>r*(2-r)}=l,{cap:Z=!0,taper:D=0,easing:gt=r=>--r*r*r+1}=j;if(S/=2,t.length===0)return[];let ot=t[t.length-1].runningLength,f=[],y=[],$=t.slice(0,10).reduce((r,d)=>{let c=d.pressure;if(i){let s=F(1,d.distance/n),a=F(1,1-s);c=F(1,r+(a-r)*(s*bt))}return(r+c)/2},t[0].pressure),O=Y(n,g,t[t.length-1].pressure,k),st,ut=t[0].vector,V=t[0].point,G=V,p=V,m=G,U=!0;for(let r=0;r<t.length-1;r++){let{pressure:d}=t[r],{point:c,vector:s,distance:a,runningLength:o}=t[r];if(r>0&&U&&o<n/2)continue;if(U&&(U=!1),g){if(i){let I=F(1,a/n),w=F(1,1-I);d=F(1,$+(w-$)*(I*bt))}O=Y(n,g,d,k)}else O=n/2;st===void 0&&(st=O);let u=o<P?Q(o/P):1,H=ot-o<D?gt((ot-o)/D):1;O=Math.max(.01,O*Math.min(u,H));let L=t[r+1].vector,A=lt(s,L);if(A<0){let I=z(N(ut),O);for(let w=0;w<1;w+=.2)m=_(v(c,I),c,J*-w),p=_(x(c,I),c,J*w),y.push(m),f.push(p);V=p,G=m;continue}let pt=z(N(R(L,s,A)),O);p=x(c,pt),m=v(c,pt);let mt=r<2||A<.25,ct=Math.pow(Math.max((o>n?n:n/2)*E,1),2);(mt||tt(V,p)>ct)&&(f.push(R(V,p,S)),V=p),(mt||tt(G,m)>ct)&&(y.push(R(G,m,S)),G=m),$=d,ut=s}let h=t[0],it=t[t.length-1],B=U||y.length<2||f.length<2;if(B&&(!(P||D)||q)){let r=0;for(let s=0;s<t.length;s++){let{pressure:a,runningLength:o}=t[s];if(o>n){r=Y(n,g,a,k);break}}let d=b(h.point,N(T(x(h.point,it.point))),-(r||O)),c=[];for(let s=0,a=.1;s<=1;s+=a)c.push(_(d,h.point,J*2*s));return c}let W=[],X=[];if(f.length>1&&y.length>1){m=y[1];for(let o=1;o<f.length;o++)if(!C(m,f[o])){p=f[o];break}if(M||P)if(!P&&!(D&&B)){if(!C(m,p)){let o=b(h.point,T(x(p,m)),-K(m,p)/2);for(let u=0,H=.1;u<=1;u+=H){let L=_(o,h.point,J*u);if(K(L,p)<1)break;W.push(L)}f.shift(),y.shift()}}else W.push(h.point,v(h.point,[.1,.1]));else if(!C(m,p)){let o=T(x(p,m)),u=K(m,p)/2;W.concat(b(h.point,o,u*.95),b(h.point,o,u),b(h.point,o,-u),b(h.point,o,-u*.95)),f.shift(),y.shift()}let r=f[f.length-1],d=y[y.length-1],c=ft(r,d),s=it.point,a=N(T(x(s,c)));if(Z||D)if(!D&&!(P&&B)){let o=b(s,a,O);for(let u=0,H=.1;u<=1;u+=H){let L=_(o,s,J*3*u);if(K(L,d)<1)break;X.push(L)}}else X.push(s);else{let o=R(c,s,.95),u=O*.95;X.concat(b(o,a,u),b(s,a,u),b(s,a,-u),b(o,a,-u))}}return f.concat(X,y.reverse(),W)}function nt(t,e={}){let{streamline:n=.5}=e,{simulatePressure:E=!0,last:g=!1}=e;if(t.length===0)return[];n=n/(E?3:2);let i=Array.isArray(t[0])?t:t.map(({x:S,y:M,pressure:P=.5})=>[S,M,P]);i.length===1&&i.push([...v(i[0],[1,1]),i[0][2]||.5]);let k=[],l={point:[i[0][0],i[0][1]],pressure:i[0][2]||.5,vector:[0,0],distance:0,runningLength:0};k.push(l);let j=i.length,q;for(let S=0;S<j;S++){q=i[S];let M=g&&S===j-1?q:R(l.point,q,1-n);if(C(l.point,M))continue;let P=T(x(l.point,M)),Q=K(M,l.point),Z=l.runningLength+Q;l={point:M,pressure:q[2]||.5,vector:P,distance:Q,runningLength:Z},k.push(l)}return k}function rt(t,e={}){return et(nt(t,e),e)}var Pt={getStroke:rt}; |
@@ -1,318 +0,1 @@ | ||
// src/vec.ts | ||
function add(A, B) { | ||
return [A[0] + B[0], A[1] + B[1]]; | ||
} | ||
function sub(A, B) { | ||
return [A[0] - B[0], A[1] - B[1]]; | ||
} | ||
function vec(A, B) { | ||
return [B[0] - A[0], B[1] - A[1]]; | ||
} | ||
function mul(A, n) { | ||
return [A[0] * n, A[1] * n]; | ||
} | ||
function div(A, n) { | ||
return [A[0] / n, A[1] / n]; | ||
} | ||
function per(A) { | ||
return [A[1], -A[0]]; | ||
} | ||
function dpr(A, B) { | ||
return A[0] * B[0] + A[1] * B[1]; | ||
} | ||
function len(A) { | ||
return Math.hypot(A[0], A[1]); | ||
} | ||
function len2(A) { | ||
return A[0] * A[0] + A[1] * A[1]; | ||
} | ||
function dist2(A, B) { | ||
return len2(sub(A, B)); | ||
} | ||
function uni(A) { | ||
return div(A, len(A)); | ||
} | ||
function dist(A, B) { | ||
return Math.hypot(A[1] - B[1], A[0] - B[0]); | ||
} | ||
function med(A, B) { | ||
return mul(add(A, B), 0.5); | ||
} | ||
function rotAround(A, C, r) { | ||
const s = Math.sin(r); | ||
const c = Math.cos(r); | ||
const px = A[0] - C[0]; | ||
const py = A[1] - C[1]; | ||
const nx = px * c - py * s; | ||
const ny = px * s + py * c; | ||
return [nx + C[0], ny + C[1]]; | ||
} | ||
function lrp(A, B, t) { | ||
return add(A, mul(vec(A, B), t)); | ||
} | ||
function isEqual(a, b) { | ||
return a[0] === b[0] && a[1] === b[1]; | ||
} | ||
// src/utils.ts | ||
function lerp(y1, y2, mu) { | ||
return y1 * (1 - mu) + y2 * mu; | ||
} | ||
function clamp(n, a, b) { | ||
return Math.max(a, Math.min(b, n)); | ||
} | ||
function toPointsArray(points) { | ||
if (Array.isArray(points[0])) { | ||
return points.map(([x, y, pressure = 0.5]) => [ | ||
x, | ||
y, | ||
pressure | ||
]); | ||
} else { | ||
return points.map(({ x, y, pressure = 0.5 }) => [x, y, pressure]); | ||
} | ||
} | ||
function getStrokeRadius(size, thinning, easing, pressure = 0.5) { | ||
if (!thinning) | ||
return size / 2; | ||
pressure = clamp(easing(pressure), 0, 1); | ||
return (thinning < 0 ? lerp(size, size + size * clamp(thinning, -0.95, -0.05), pressure) : lerp(size - size * clamp(thinning, 0.05, 0.95), size, pressure)) / 2; | ||
} | ||
// src/index.ts | ||
var { min, PI } = Math; | ||
function getStrokePoints(points, options = {}) { | ||
let { streamline = 0.5 } = options; | ||
const { simulatePressure = true, last: isComplete = false } = options; | ||
if (points.length === 0) | ||
return []; | ||
streamline = streamline / (simulatePressure ? 3 : 2); | ||
const pts = toPointsArray(points); | ||
if (pts.length === 1) | ||
pts.push([...add(pts[0], [1, 1]), pts[0][2]]); | ||
const strokePoints = []; | ||
let prev = { | ||
point: [pts[0][0], pts[0][1]], | ||
pressure: pts[0][2], | ||
vector: [0, 0], | ||
distance: 0, | ||
runningLength: 0 | ||
}; | ||
strokePoints.push(prev); | ||
const len3 = pts.length; | ||
let curr; | ||
for (let i = 0; i < len3; i++) { | ||
curr = pts[i]; | ||
const point = isComplete && i === len3 - 1 ? curr : lrp(prev.point, curr, 1 - streamline); | ||
if (isEqual(prev.point, point)) | ||
continue; | ||
const vector = uni(sub(prev.point, point)); | ||
const distance = dist(point, prev.point); | ||
const runningLength = prev.runningLength + distance; | ||
prev = { | ||
point, | ||
pressure: curr[2], | ||
vector, | ||
distance, | ||
runningLength | ||
}; | ||
strokePoints.push(prev); | ||
} | ||
return strokePoints; | ||
} | ||
var RATE_OF_CHANGE = 0.3; | ||
function getStrokeOutlinePoints(points, options = {}) { | ||
const { | ||
size = 8, | ||
thinning = 0.5, | ||
smoothing = 0.5, | ||
simulatePressure = true, | ||
easing = (t) => t, | ||
start = {}, | ||
end = {}, | ||
last: isComplete = false | ||
} = options; | ||
let { streamline = 0.5 } = options; | ||
streamline /= 2; | ||
const { | ||
cap: capStart = true, | ||
taper: taperStart = 0, | ||
easing: taperStartEase = (t) => t * (2 - t) | ||
} = start; | ||
const { | ||
cap: capEnd = true, | ||
taper: taperEnd = 0, | ||
easing: taperEndEase = (t) => --t * t * t + 1 | ||
} = end; | ||
const len3 = points.length; | ||
if (len3 === 0) | ||
return []; | ||
const totalLength = points[len3 - 1].runningLength; | ||
const leftPts = []; | ||
const rightPts = []; | ||
let prevPressure = points.slice(0, 10).reduce((acc, curr) => { | ||
let pressure = curr.pressure; | ||
if (simulatePressure) { | ||
const sp = min(1, curr.distance / size); | ||
const rp = min(1, 1 - sp); | ||
pressure = min(1, acc + (rp - acc) * (sp * RATE_OF_CHANGE)); | ||
} | ||
return (acc + pressure) / 2; | ||
}, points[0].pressure); | ||
let radius = getStrokeRadius(size, thinning, easing, points[len3 - 1].pressure); | ||
let firstRadius = void 0; | ||
let prevVector = points[0].vector; | ||
let pl = points[0].point; | ||
let pr = pl; | ||
let tl = pl; | ||
let tr = pr; | ||
let short = true; | ||
for (let i = 0; i < len3 - 1; i++) { | ||
let { pressure } = points[i]; | ||
const { point, vector, distance, runningLength } = points[i]; | ||
if (i > 0 && short && runningLength < size / 2) { | ||
continue; | ||
} else if (short) { | ||
short = false; | ||
} | ||
if (thinning) { | ||
if (simulatePressure) { | ||
const sp = min(1, distance / size); | ||
const rp = min(1, 1 - sp); | ||
pressure = min(1, prevPressure + (rp - prevPressure) * (sp * RATE_OF_CHANGE)); | ||
} | ||
radius = getStrokeRadius(size, thinning, easing, pressure); | ||
} else { | ||
radius = size / 2; | ||
} | ||
if (firstRadius === void 0) { | ||
firstRadius = radius; | ||
} | ||
const ts = runningLength < taperStart ? taperStartEase(runningLength / taperStart) : 1; | ||
const te = totalLength - runningLength < taperEnd ? taperEndEase((totalLength - runningLength) / taperEnd) : 1; | ||
radius = Math.max(0.01, radius * Math.min(ts, te)); | ||
const nextVector = points[i + 1].vector; | ||
const dpr2 = dpr(vector, nextVector); | ||
if (dpr2 < 0) { | ||
const offset2 = mul(per(prevVector), radius); | ||
for (let t = 0; t < 1; t += 0.2) { | ||
tr = rotAround(add(point, offset2), point, PI * -t); | ||
tl = rotAround(sub(point, offset2), point, PI * t); | ||
rightPts.push(tr); | ||
leftPts.push(tl); | ||
} | ||
pl = tl; | ||
pr = tr; | ||
continue; | ||
} | ||
const offset = mul(per(lrp(nextVector, vector, dpr2)), radius); | ||
tl = sub(point, offset); | ||
tr = add(point, offset); | ||
const alwaysAdd = i < 2 || dpr2 < 0.25; | ||
const minDistance = Math.pow(Math.max((runningLength > size ? size : size / 2) * smoothing, 1), 2); | ||
if (alwaysAdd || dist2(pl, tl) > minDistance) { | ||
leftPts.push(lrp(pl, tl, streamline)); | ||
pl = tl; | ||
} | ||
if (alwaysAdd || dist2(pr, tr) > minDistance) { | ||
rightPts.push(lrp(pr, tr, streamline)); | ||
pr = tr; | ||
} | ||
prevPressure = pressure; | ||
prevVector = vector; | ||
} | ||
const firstPoint = points[0]; | ||
const lastPoint = points[len3 - 1]; | ||
const isVeryShort = short || rightPts.length < 2 || leftPts.length < 2; | ||
if (isVeryShort && (!(taperStart || taperEnd) || isComplete)) { | ||
let ir = 0; | ||
for (let i = 0; i < len3; i++) { | ||
const { pressure, runningLength } = points[i]; | ||
if (runningLength > size) { | ||
ir = getStrokeRadius(size, thinning, easing, pressure); | ||
break; | ||
} | ||
} | ||
const start2 = sub(firstPoint.point, mul(per(uni(vec(lastPoint.point, firstPoint.point))), ir || radius)); | ||
const dotPts = []; | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
dotPts.push(rotAround(start2, firstPoint.point, PI * 2 * t)); | ||
} | ||
return dotPts; | ||
} | ||
const startCap = []; | ||
const endCap = []; | ||
if (leftPts.length > 1 && rightPts.length > 1) { | ||
tr = rightPts[1]; | ||
for (let i = 1; i < leftPts.length; i++) { | ||
if (!isEqual(tr, leftPts[i])) { | ||
tl = leftPts[i]; | ||
break; | ||
} | ||
} | ||
if (capStart || taperStart) { | ||
if (!taperStart && !(taperEnd && isVeryShort)) { | ||
if (!isEqual(tr, tl)) { | ||
const start2 = sub(firstPoint.point, mul(uni(vec(tr, tl)), dist(tr, tl) / 2)); | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
const pt = rotAround(start2, firstPoint.point, PI * t); | ||
if (dist(pt, tl) < 1) | ||
break; | ||
startCap.push(pt); | ||
} | ||
leftPts.shift(); | ||
rightPts.shift(); | ||
} | ||
} else { | ||
startCap.push(firstPoint.point, add(firstPoint.point, [0.1, 0.1])); | ||
} | ||
} else { | ||
if (!isEqual(tr, tl)) { | ||
const vector2 = uni(vec(tr, tl)); | ||
const dist3 = dist(tr, tl) / 2; | ||
startCap.push(sub(firstPoint.point, mul(vector2, dist3 * 0.95))); | ||
startCap.push(sub(firstPoint.point, mul(vector2, dist3))); | ||
startCap.push(add(firstPoint.point, mul(vector2, dist3))); | ||
startCap.push(add(firstPoint.point, mul(vector2, dist3 * 0.95))); | ||
leftPts.shift(); | ||
rightPts.shift(); | ||
} | ||
} | ||
const ll = leftPts[leftPts.length - 1]; | ||
const lr = rightPts[rightPts.length - 1]; | ||
const mid = med(ll, lr); | ||
const last = lastPoint.point; | ||
const vector = uni(sub(last, mid)); | ||
if (capEnd || taperEnd) { | ||
if (!taperEnd && !(taperStart && isVeryShort)) { | ||
const start2 = add(last, mul(per(vector), radius)); | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
const pt = rotAround(start2, last, PI * 3 * t); | ||
if (dist(pt, lr) < 1) | ||
break; | ||
endCap.push(pt); | ||
} | ||
} else { | ||
endCap.push(last); | ||
} | ||
} else { | ||
const justBefore = lrp(mid, last, 0.95); | ||
const r = radius * 0.95; | ||
endCap.push(add(justBefore, mul(per(vector), r))); | ||
endCap.push(add(last, mul(per(vector), r))); | ||
endCap.push(sub(last, mul(per(vector), r))); | ||
endCap.push(sub(justBefore, mul(per(vector), r))); | ||
} | ||
} | ||
return leftPts.concat(endCap, rightPts.reverse(), startCap); | ||
} | ||
function getStroke(points, options = {}) { | ||
return getStrokeOutlinePoints(getStrokePoints(points, options), options); | ||
} | ||
var src_default = getStroke; | ||
export { | ||
src_default as default, | ||
getStroke, | ||
getStrokeOutlinePoints, | ||
getStrokePoints | ||
}; | ||
function Y(t,e,o,E=g=>g){return t*E(.5-e*(.5-o))}function v(t,e){return[t[0]+e[0],t[1]+e[1]]}function x(t,e){return[t[0]-e[0],t[1]-e[1]]}function z(t,e){return[t[0]*e,t[1]*e]}function gt(t,e){return[t[0]/e,t[1]/e]}function N(t){return[t[1],-t[0]]}function pt(t,e){return t[0]*e[0]+t[1]*e[1]}function C(t,e){return t[0]===e[0]&&t[1]===e[1]}function ht(t){return Math.hypot(t[0],t[1])}function dt(t){return t[0]*t[0]+t[1]*t[1]}function tt(t,e){return dt(x(t,e))}function T(t){return gt(t,ht(t))}function K(t,e){return Math.hypot(t[1]-e[1],t[0]-e[0])}function mt(t,e){return z(v(t,e),.5)}function _(t,e,o){let E=Math.sin(o),g=Math.cos(o),i=t[0]-e[0],k=t[1]-e[1],l=i*g-k*E,j=i*E+k*g;return[l+e[0],j+e[1]]}function R(t,e,o){return v(t,z(x(e,t),o))}function b(t,e,o){return v(t,z(e,o))}var ct=.3,{min:F,PI:J}=Math;function at(t,e={}){let{size:o=16,smoothing:E=.5,thinning:g=.5,simulatePressure:i=!0,easing:k=n=>n,start:l={},end:j={},last:q=!1}=e,{streamline:S=.5}=e,{cap:M=!0,taper:P=0,easing:Q=n=>n*(2-n)}=l,{cap:Z=!0,taper:D=0,easing:bt=n=>--n*n*n+1}=j;if(S/=2,t.length===0)return[];let et=t[t.length-1].runningLength,f=[],y=[],$=t.slice(0,10).reduce((n,d)=>{let c=d.pressure;if(i){let s=F(1,d.distance/o),a=F(1,1-s);c=F(1,n+(a-n)*(s*ct))}return(n+c)/2},t[0].pressure),O=Y(o,g,t[t.length-1].pressure,k),nt,rt=t[0].vector,V=t[0].point,G=V,p=V,m=G,U=!0;for(let n=0;n<t.length-1;n++){let{pressure:d}=t[n],{point:c,vector:s,distance:a,runningLength:r}=t[n];if(n>0&&U&&r<o/2)continue;if(U&&(U=!1),g){if(i){let I=F(1,a/o),w=F(1,1-I);d=F(1,$+(w-$)*(I*ct))}O=Y(o,g,d,k)}else O=o/2;nt===void 0&&(nt=O);let u=r<P?Q(r/P):1,H=et-r<D?bt((et-r)/D):1;O=Math.max(.01,O*Math.min(u,H));let L=t[n+1].vector,A=pt(s,L);if(A<0){let I=z(N(rt),O);for(let w=0;w<1;w+=.2)m=_(v(c,I),c,J*-w),p=_(x(c,I),c,J*w),y.push(m),f.push(p);V=p,G=m;continue}let st=z(N(R(L,s,A)),O);p=x(c,st),m=v(c,st);let ut=n<2||A<.25,it=Math.pow(Math.max((r>o?o:o/2)*E,1),2);(ut||tt(V,p)>it)&&(f.push(R(V,p,S)),V=p),(ut||tt(G,m)>it)&&(y.push(R(G,m,S)),G=m),$=d,rt=s}let h=t[0],ot=t[t.length-1],B=U||y.length<2||f.length<2;if(B&&(!(P||D)||q)){let n=0;for(let s=0;s<t.length;s++){let{pressure:a,runningLength:r}=t[s];if(r>o){n=Y(o,g,a,k);break}}let d=b(h.point,N(T(x(h.point,ot.point))),-(n||O)),c=[];for(let s=0,a=.1;s<=1;s+=a)c.push(_(d,h.point,J*2*s));return c}let W=[],X=[];if(f.length>1&&y.length>1){m=y[1];for(let r=1;r<f.length;r++)if(!C(m,f[r])){p=f[r];break}if(M||P)if(!P&&!(D&&B)){if(!C(m,p)){let r=b(h.point,T(x(p,m)),-K(m,p)/2);for(let u=0,H=.1;u<=1;u+=H){let L=_(r,h.point,J*u);if(K(L,p)<1)break;W.push(L)}f.shift(),y.shift()}}else W.push(h.point,v(h.point,[.1,.1]));else if(!C(m,p)){let r=T(x(p,m)),u=K(m,p)/2;W.concat(b(h.point,r,u*.95),b(h.point,r,u),b(h.point,r,-u),b(h.point,r,-u*.95)),f.shift(),y.shift()}let n=f[f.length-1],d=y[y.length-1],c=mt(n,d),s=ot.point,a=N(T(x(s,c)));if(Z||D)if(!D&&!(P&&B)){let r=b(s,a,O);for(let u=0,H=.1;u<=1;u+=H){let L=_(r,s,J*3*u);if(K(L,d)<1)break;X.push(L)}}else X.push(s);else{let r=R(c,s,.95),u=O*.95;X.concat(b(r,a,u),b(s,a,u),b(s,a,-u),b(r,a,-u))}}return f.concat(X,y.reverse(),W)}function lt(t,e={}){let{streamline:o=.5}=e,{simulatePressure:E=!0,last:g=!1}=e;if(t.length===0)return[];o=o/(E?3:2);let i=Array.isArray(t[0])?t:t.map(({x:S,y:M,pressure:P=.5})=>[S,M,P]);i.length===1&&i.push([...v(i[0],[1,1]),i[0][2]||.5]);let k=[],l={point:[i[0][0],i[0][1]],pressure:i[0][2]||.5,vector:[0,0],distance:0,runningLength:0};k.push(l);let j=i.length,q;for(let S=0;S<j;S++){q=i[S];let M=g&&S===j-1?q:R(l.point,q,1-o);if(C(l.point,M))continue;let P=T(x(l.point,M)),Q=K(M,l.point),Z=l.runningLength+Q;l={point:M,pressure:q[2]||.5,vector:P,distance:Q,runningLength:Z},k.push(l)}return k}function ft(t,e={}){return at(lt(t,e),e)}var Rt={getStroke:ft};export{Rt as default,ft as getStroke,at as getStrokeOutlinePoints,lt as getStrokePoints}; |
@@ -1,51 +0,10 @@ | ||
import type { StrokeOptions, StrokePoint } from './types'; | ||
/** | ||
* ## getStrokePoints | ||
* @description Get points for a stroke. Returns an array of objects with an adjusted point, pressure, vector, distance, and runningLength. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param streamline How much to streamline the stroke. | ||
* @param size The stroke's size. | ||
*/ | ||
export declare function getStrokePoints(points: number[][] | { | ||
x: number; | ||
y: number; | ||
pressure?: number; | ||
}[], options?: StrokeOptions): StrokePoint[]; | ||
/** | ||
* ## getStrokeOutlinePoints | ||
* @description Get an array of points (as `[x, y]`) representing the outline of a stroke. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.easing An easing function to apply to each point's pressure. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
*/ | ||
export declare function getStrokeOutlinePoints(points: StrokePoint[], options?: Partial<StrokeOptions>): number[][]; | ||
/** | ||
* ## getStroke | ||
* @description Returns a stroke as an array of outline points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional in both cases. | ||
* @param options (optional) An object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.easing An easing function to apply to each point's pressure. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
*/ | ||
declare function getStroke(points: number[][], options?: StrokeOptions): number[][]; | ||
declare function getStroke(points: { | ||
x: number; | ||
y: number; | ||
pressure?: number; | ||
}[], options?: StrokeOptions): number[][]; | ||
export default getStroke; | ||
export { getStroke }; | ||
export { StrokeOptions }; | ||
import { getStroke } from './getStroke'; | ||
declare const _default: { | ||
getStroke: typeof getStroke; | ||
}; | ||
export default _default; | ||
export * from './getStrokeOutlinePoints'; | ||
export * from './getStrokePoints'; | ||
export * from './getStroke'; | ||
export * from './types'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -0,1 +1,4 @@ | ||
/** | ||
* The options object for `getStroke` or `getStrokePoints`. | ||
*/ | ||
export interface StrokeOptions { | ||
@@ -20,2 +23,5 @@ size?: number; | ||
} | ||
/** | ||
* The points returned by `getStrokePoints`, and the input for `getStrokeOutlinePoints`. | ||
*/ | ||
export interface StrokePoint { | ||
@@ -28,6 +34,2 @@ point: number[]; | ||
} | ||
export declare type InputPoint = { | ||
x: number; | ||
y: number; | ||
pressure?: number; | ||
} | number[]; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -1,96 +0,2 @@ | ||
/** | ||
* Negate a vector. | ||
* @param A | ||
*/ | ||
export declare function neg(A: number[]): number[]; | ||
/** | ||
* Add vectors. | ||
* @param A | ||
* @param B | ||
*/ | ||
export declare function add(A: number[], B: number[]): number[]; | ||
/** | ||
* Subtract vectors. | ||
* @param A | ||
* @param B | ||
*/ | ||
export declare function sub(A: number[], B: number[]): number[]; | ||
/** | ||
* Get the vector from vectors A to B. | ||
* @param A | ||
* @param B | ||
*/ | ||
export declare function vec(A: number[], B: number[]): number[]; | ||
/** | ||
* Vector multiplication by scalar | ||
* @param A | ||
* @param n | ||
*/ | ||
export declare function mul(A: number[], n: number): number[]; | ||
/** | ||
* Vector division by scalar. | ||
* @param A | ||
* @param n | ||
*/ | ||
export declare function div(A: number[], n: number): number[]; | ||
/** | ||
* Perpendicular rotation of a vector A | ||
* @param A | ||
*/ | ||
export declare function per(A: number[]): number[]; | ||
/** | ||
* Dot product | ||
* @param A | ||
* @param B | ||
*/ | ||
export declare function dpr(A: number[], B: number[]): number; | ||
/** | ||
* Length of the vector | ||
* @param A | ||
*/ | ||
export declare function len(A: number[]): number; | ||
/** | ||
* Length of the vector squared | ||
* @param A | ||
*/ | ||
export declare function len2(A: number[]): number; | ||
/** | ||
* Dist length from A to B squared. | ||
* @param A | ||
* @param B | ||
*/ | ||
export declare function dist2(A: number[], B: number[]): number; | ||
/** | ||
* Get normalized / unit vector. | ||
* @param A | ||
*/ | ||
export declare function uni(A: number[]): number[]; | ||
/** | ||
* Dist length from A to B | ||
* @param A | ||
* @param B | ||
*/ | ||
export declare function dist(A: number[], B: number[]): number; | ||
/** | ||
* Mean between two vectors or mid vector between two vectors | ||
* @param A | ||
* @param B | ||
*/ | ||
export declare function med(A: number[], B: number[]): number[]; | ||
/** | ||
* Rotate a vector around another vector by r (radians) | ||
* @param A vector | ||
* @param C center | ||
* @param r rotation in radians | ||
*/ | ||
export declare function rotAround(A: number[], C: number[], r: number): number[]; | ||
/** | ||
* Interpolate vector A to B with a scalar t | ||
* @param A | ||
* @param B | ||
* @param t scalar | ||
*/ | ||
export declare function lrp(A: number[], B: number[], t: number): number[]; | ||
export declare function isLeft(p1: number[], pc: number[], p2: number[]): number; | ||
export declare function clockwise(p1: number[], pc: number[], p2: number[]): boolean; | ||
export declare function isEqual(a: number[], b: number[]): boolean; | ||
export {}; | ||
//# sourceMappingURL=vec.d.ts.map |
{ | ||
"version": "0.5.4", | ||
"version": "1.0.0", | ||
"name": "perfect-freehand", | ||
@@ -64,3 +64,3 @@ "private": false, | ||
}, | ||
"gitHead": "b52b4d22b7b12fbfc5cff300c7416ca7786aba15" | ||
"gitHead": "4230a43d94d943c6329585a38370983c37bb3433" | ||
} |
516
src/index.ts
@@ -1,512 +0,8 @@ | ||
import { toPointsArray, getStrokeRadius } from './utils' | ||
import type { StrokeOptions, StrokePoint } from './types' | ||
import * as vec from './vec' | ||
import { getStroke } from './getStroke' | ||
const { min, PI } = Math | ||
export default { getStroke } | ||
/** | ||
* ## getStrokePoints | ||
* @description Get points for a stroke. Returns an array of objects with an adjusted point, pressure, vector, distance, and runningLength. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param streamline How much to streamline the stroke. | ||
* @param size The stroke's size. | ||
*/ | ||
export function getStrokePoints( | ||
points: number[][] | { x: number; y: number; pressure?: number }[], | ||
options = {} as StrokeOptions | ||
): StrokePoint[] { | ||
let { streamline = 0.5 } = options | ||
const { simulatePressure = true, last: isComplete = false } = options | ||
// If we don't have any points, return an empty array. | ||
if (points.length === 0) return [] | ||
// Knock down the streamline (more if we're simulating pressure). | ||
streamline = streamline / (simulatePressure ? 3 : 2) | ||
// Whatever the input is, make sure that the points are in number[][]. | ||
const pts = toPointsArray(points) | ||
// If there's only one point, add another point at a 1pt offset. | ||
if (pts.length === 1) pts.push([...vec.add(pts[0], [1, 1]), pts[0][2]]) | ||
// The strokePoints array will hold the points for the stroke. | ||
// Start it out with the first point, which needs no adjustment. | ||
const strokePoints: StrokePoint[] = [] | ||
let prev: StrokePoint = { | ||
point: [pts[0][0], pts[0][1]], | ||
pressure: pts[0][2], | ||
vector: [0, 0], | ||
distance: 0, | ||
runningLength: 0, | ||
} | ||
strokePoints.push(prev) | ||
// Iterate through all of the points. | ||
const len = pts.length | ||
let curr: number[] | ||
for (let i = 0; i < len; i++) { | ||
curr = pts[i] | ||
// If we're at the last point, then add the actual input point. | ||
// Otherwise, using the streamline option, interpolate a new point | ||
// between the previous point the current point. | ||
// More streamline = closer to the previous point; | ||
// less streamline = closer to the current point. | ||
// This takes a lot of the "noise" out of the input points. | ||
const point = | ||
isComplete && i === len - 1 | ||
? curr | ||
: vec.lrp(prev.point, curr, 1 - streamline) | ||
// If the new point is the same as the previous point, skip ahead. | ||
if (vec.isEqual(prev.point, point)) continue | ||
// What's the vector from the current point to the previous point? | ||
const vector = vec.uni(vec.sub(prev.point, point)) | ||
// How far is the new point from the previous point? | ||
const distance = vec.dist(point, prev.point) | ||
// Add this distance to the total "running length" of the line. | ||
const runningLength = prev.runningLength + distance | ||
// Create a new strokepoint (it will be the new "previous" one) | ||
prev = { | ||
point, | ||
pressure: curr[2], | ||
vector, | ||
distance, | ||
runningLength, | ||
} | ||
strokePoints.push(prev) | ||
} | ||
return strokePoints | ||
} | ||
const RATE_OF_CHANGE = 0.3 | ||
/** | ||
* ## getStrokeOutlinePoints | ||
* @description Get an array of points (as `[x, y]`) representing the outline of a stroke. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.easing An easing function to apply to each point's pressure. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
*/ | ||
export function getStrokeOutlinePoints( | ||
points: StrokePoint[], | ||
options: Partial<StrokeOptions> = {} as Partial<StrokeOptions> | ||
): number[][] { | ||
const { | ||
size = 8, | ||
thinning = 0.5, | ||
smoothing = 0.5, | ||
simulatePressure = true, | ||
easing = (t) => t, | ||
start = {}, | ||
end = {}, | ||
last: isComplete = false, | ||
} = options | ||
let { streamline = 0.5 } = options | ||
streamline /= 2 | ||
const { | ||
cap: capStart = true, | ||
taper: taperStart = 0, | ||
easing: taperStartEase = (t) => t * (2 - t), | ||
} = start | ||
const { | ||
cap: capEnd = true, | ||
taper: taperEnd = 0, | ||
easing: taperEndEase = (t) => --t * t * t + 1, | ||
} = end | ||
// The number of points in the array | ||
const len = points.length | ||
// We can't do anything with an empty array. | ||
if (len === 0) return [] | ||
// The total length of the line | ||
const totalLength = points[len - 1].runningLength | ||
// Our collected left and right points | ||
const leftPts: number[][] = [] | ||
const rightPts: number[][] = [] | ||
// Previous pressure (start with average of first five pressures, | ||
// in order to prevent fat starts for every line. Drawn lines | ||
// almost always start slow! | ||
let prevPressure = points.slice(0, 10).reduce((acc, curr) => { | ||
let pressure = curr.pressure | ||
if (simulatePressure) { | ||
// Speed of change - how fast should the the pressure changing? | ||
const sp = min(1, curr.distance / size) | ||
// Rate of change - how much of a change is there? | ||
const rp = min(1, 1 - sp) | ||
// Accelerate the pressure | ||
pressure = min(1, acc + (rp - acc) * (sp * RATE_OF_CHANGE)) | ||
} | ||
return (acc + pressure) / 2 | ||
}, points[0].pressure) | ||
// The current radius | ||
let radius = getStrokeRadius(size, thinning, easing, points[len - 1].pressure) | ||
// The radius of the first saved point | ||
let firstRadius: number | undefined = undefined | ||
// Previous vector | ||
let prevVector = points[0].vector | ||
// Previous left and right points | ||
let pl = points[0].point | ||
let pr = pl | ||
// Temporary left and right points | ||
let tl = pl | ||
let tr = pr | ||
let short = true | ||
/* | ||
Find the outline's left and right points | ||
Iterating through the points and populate the rightPts and leftPts arrays, | ||
skipping the first and last pointsm, which will get caps later on. | ||
*/ | ||
for (let i = 0; i < len - 1; i++) { | ||
let { pressure } = points[i] | ||
const { point, vector, distance, runningLength } = points[i] | ||
if (i > 0 && short && runningLength < size / 2) { | ||
continue | ||
} else if (short) { | ||
short = false | ||
} | ||
/* | ||
Calculate the radius | ||
If not thinning, the current point's radius will be half the size; or | ||
otherwise, the size will be based on the current (real or simulated) | ||
pressure. | ||
*/ | ||
if (thinning) { | ||
// If we're simulating pressure, then do so based on the distance | ||
// between the current point and the previous point, and the size | ||
// of the stroke. | ||
if (simulatePressure) { | ||
const sp = min(1, distance / size) | ||
const rp = min(1, 1 - sp) | ||
pressure = min( | ||
1, | ||
prevPressure + (rp - prevPressure) * (sp * RATE_OF_CHANGE) | ||
) | ||
} | ||
radius = getStrokeRadius(size, thinning, easing, pressure) | ||
} else { | ||
radius = size / 2 | ||
} | ||
if (firstRadius === undefined) { | ||
firstRadius = radius | ||
} | ||
/* | ||
Apply tapering | ||
If the current length is within the taper distance at either the | ||
start or the end, calculate the taper strengths. Apply the smaller | ||
of the two taper strengths to the radius. | ||
*/ | ||
const ts = | ||
runningLength < taperStart | ||
? taperStartEase(runningLength / taperStart) | ||
: 1 | ||
const te = | ||
totalLength - runningLength < taperEnd | ||
? taperEndEase((totalLength - runningLength) / taperEnd) | ||
: 1 | ||
radius = Math.max(0.01, radius * Math.min(ts, te)) | ||
/* | ||
Handle sharp corners | ||
Find the difference (dot product) between the current and next vector. | ||
If the next vector is at more than a right angle to the current vector, | ||
draw a cap at the current point. | ||
*/ | ||
const nextVector = points[i + 1].vector | ||
const dpr = vec.dpr(vector, nextVector) | ||
if (dpr < 0) { | ||
const offset = vec.mul(vec.per(prevVector), radius) | ||
for (let t = 0; t < 1; t += 0.2) { | ||
tr = vec.rotAround(vec.add(point, offset), point, PI * -t) | ||
tl = vec.rotAround(vec.sub(point, offset), point, PI * t) | ||
rightPts.push(tr) | ||
leftPts.push(tl) | ||
} | ||
pl = tl | ||
pr = tr | ||
continue | ||
} | ||
/* | ||
Add regular points | ||
Project points to either side of the current point, using the | ||
calculated size as a distance. If a point's distance to the | ||
previous point on that side greater than the minimum distance | ||
(or if the corner is kinda sharp), add the points to the side's | ||
points array. | ||
*/ | ||
const offset = vec.mul(vec.per(vec.lrp(nextVector, vector, dpr)), radius) | ||
tl = vec.sub(point, offset) | ||
tr = vec.add(point, offset) | ||
const alwaysAdd = i < 2 || dpr < 0.25 | ||
const minDistance = Math.pow( | ||
Math.max((runningLength > size ? size : size / 2) * smoothing, 1), | ||
2 | ||
) | ||
if (alwaysAdd || vec.dist2(pl, tl) > minDistance) { | ||
leftPts.push(vec.lrp(pl, tl, streamline)) | ||
pl = tl | ||
} | ||
if (alwaysAdd || vec.dist2(pr, tr) > minDistance) { | ||
rightPts.push(vec.lrp(pr, tr, streamline)) | ||
pr = tr | ||
} | ||
// Set variables for next iteration | ||
prevPressure = pressure | ||
prevVector = vector | ||
} | ||
/* | ||
Drawing caps | ||
Now that we have our points on either side of the line, we need to | ||
draw caps at the start and end. Tapered lines don't have caps, but | ||
may have dots for very short lines. | ||
*/ | ||
const firstPoint = points[0] | ||
const lastPoint = points[len - 1] | ||
const isVeryShort = short || rightPts.length < 2 || leftPts.length < 2 | ||
/* | ||
Draw a dot for very short or completed strokes | ||
If the line is too short to gather left or right points and if the line is | ||
not tapered on either side, draw a dot. If the line is tapered, then only | ||
draw a dot if the line is both very short and complete. If we draw a dot, | ||
we can just return those points. | ||
*/ | ||
if (isVeryShort && (!(taperStart || taperEnd) || isComplete)) { | ||
let ir = 0 | ||
for (let i = 0; i < len; i++) { | ||
const { pressure, runningLength } = points[i] | ||
if (runningLength > size) { | ||
ir = getStrokeRadius(size, thinning, easing, pressure) | ||
break | ||
} | ||
} | ||
const start = vec.sub( | ||
firstPoint.point, | ||
vec.mul( | ||
vec.per(vec.uni(vec.vec(lastPoint.point, firstPoint.point))), | ||
ir || radius | ||
) | ||
) | ||
const dotPts: number[][] = [] | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
dotPts.push(vec.rotAround(start, firstPoint.point, PI * 2 * t)) | ||
} | ||
return dotPts | ||
} | ||
/* | ||
Draw a start cap | ||
Unless the line has a tapered start, or unless the line has a tapered end | ||
and the line is very short, draw a start cap around the first point. Use | ||
the distance between the second left and right point for the cap's radius. | ||
Finally remove the first left and right points. :psyduck: | ||
*/ | ||
const startCap: number[][] = [] | ||
const endCap: number[][] = [] | ||
if (leftPts.length > 1 && rightPts.length > 1) { | ||
tr = rightPts[1] | ||
for (let i = 1; i < leftPts.length; i++) { | ||
if (!vec.isEqual(tr, leftPts[i])) { | ||
tl = leftPts[i] | ||
break | ||
} | ||
} | ||
if (capStart || taperStart) { | ||
if (!taperStart && !(taperEnd && isVeryShort)) { | ||
if (!vec.isEqual(tr, tl)) { | ||
const start = vec.sub( | ||
firstPoint.point, | ||
vec.mul(vec.uni(vec.vec(tr, tl)), vec.dist(tr, tl) / 2) | ||
) | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
const pt = vec.rotAround(start, firstPoint.point, PI * t) | ||
if (vec.dist(pt, tl) < 1) break | ||
startCap.push(pt) | ||
} | ||
leftPts.shift() | ||
rightPts.shift() | ||
} | ||
} else { | ||
startCap.push(firstPoint.point, vec.add(firstPoint.point, [0.1, 0.1])) | ||
} | ||
} else { | ||
if (!vec.isEqual(tr, tl)) { | ||
const vector = vec.uni(vec.vec(tr, tl)) | ||
const dist = vec.dist(tr, tl) / 2 | ||
startCap.push(vec.sub(firstPoint.point, vec.mul(vector, dist * 0.95))) | ||
startCap.push(vec.sub(firstPoint.point, vec.mul(vector, dist))) | ||
startCap.push(vec.add(firstPoint.point, vec.mul(vector, dist))) | ||
startCap.push(vec.add(firstPoint.point, vec.mul(vector, dist * 0.95))) | ||
leftPts.shift() | ||
rightPts.shift() | ||
} | ||
} | ||
/* | ||
Draw an end cap | ||
If the line does not have a tapered end, and unless the line has a tapered | ||
start and the line is very short, draw a cap around the last point. Finally, | ||
remove the last left and right points. Otherwise, add the last point. Note | ||
that This cap is a full-turn-and-a-half: this prevents incorrect caps on | ||
sharp end turns. | ||
*/ | ||
// The last left point | ||
const ll = leftPts[leftPts.length - 1] | ||
// The last right point | ||
const lr = rightPts[rightPts.length - 1] | ||
// The point between the two | ||
const mid = vec.med(ll, lr) | ||
// The last provided point | ||
const last = lastPoint.point | ||
const vector = vec.uni(vec.sub(last, mid)) | ||
if (capEnd || taperEnd) { | ||
if (!taperEnd && !(taperStart && isVeryShort)) { | ||
// Draw the end cap | ||
const start = vec.add(last, vec.mul(vec.per(vector), radius)) | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
const pt = vec.rotAround(start, last, PI * 3 * t) | ||
if (vec.dist(pt, lr) < 1) break | ||
endCap.push(pt) | ||
} | ||
} else { | ||
// Just push the last point to the line | ||
endCap.push(last) | ||
} | ||
} else { | ||
const justBefore = vec.lrp(mid, last, 0.95) | ||
const r = radius * 0.95 | ||
endCap.push(vec.add(justBefore, vec.mul(vec.per(vector), r))) | ||
endCap.push(vec.add(last, vec.mul(vec.per(vector), r))) | ||
endCap.push(vec.sub(last, vec.mul(vec.per(vector), r))) | ||
endCap.push(vec.sub(justBefore, vec.mul(vec.per(vector), r))) | ||
} | ||
} | ||
/* | ||
Return the points in the correct windind order: begin on the left side, then | ||
continue around the end cap, then come back along the right side, and finally | ||
complete the start cap. | ||
*/ | ||
return leftPts.concat(endCap, rightPts.reverse(), startCap) | ||
} | ||
/** | ||
* ## getStroke | ||
* @description Returns a stroke as an array of outline points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional in both cases. | ||
* @param options (optional) An object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.easing An easing function to apply to each point's pressure. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
*/ | ||
function getStroke(points: number[][], options?: StrokeOptions): number[][] | ||
function getStroke( | ||
points: { x: number; y: number; pressure?: number }[], | ||
options?: StrokeOptions | ||
): number[][] | ||
function getStroke( | ||
points: number[][] | { x: number; y: number; pressure?: number }[], | ||
options: StrokeOptions = {} as StrokeOptions | ||
): number[][] { | ||
return getStrokeOutlinePoints(getStrokePoints(points, options), options) | ||
} | ||
export default getStroke | ||
export { getStroke } | ||
export { StrokeOptions } | ||
export * from './getStrokeOutlinePoints' | ||
export * from './getStrokePoints' | ||
export * from './getStroke' | ||
export * from './types' |
@@ -0,1 +1,4 @@ | ||
/** | ||
* The options object for `getStroke` or `getStrokePoints`. | ||
*/ | ||
export interface StrokeOptions { | ||
@@ -21,2 +24,5 @@ size?: number | ||
/** | ||
* The points returned by `getStrokePoints`, and the input for `getStrokeOutlinePoints`. | ||
*/ | ||
export interface StrokePoint { | ||
@@ -29,3 +35,1 @@ point: number[] | ||
} | ||
export type InputPoint = { x: number; y: number; pressure?: number } | number[] |
/** | ||
* Negate a vector. | ||
* @param A | ||
* @internal | ||
*/ | ||
@@ -13,2 +14,3 @@ export function neg(A: number[]) { | ||
* @param B | ||
* @internal | ||
*/ | ||
@@ -23,2 +25,3 @@ export function add(A: number[], B: number[]) { | ||
* @param B | ||
* @internal | ||
*/ | ||
@@ -30,15 +33,6 @@ export function sub(A: number[], B: number[]) { | ||
/** | ||
* Get the vector from vectors A to B. | ||
* @param A | ||
* @param B | ||
*/ | ||
export function vec(A: number[], B: number[]) { | ||
// A, B as vectors get the vector from A to B | ||
return [B[0] - A[0], B[1] - A[1]] | ||
} | ||
/** | ||
* Vector multiplication by scalar | ||
* @param A | ||
* @param n | ||
* @internal | ||
*/ | ||
@@ -53,2 +47,3 @@ export function mul(A: number[], n: number) { | ||
* @param n | ||
* @internal | ||
*/ | ||
@@ -62,2 +57,3 @@ export function div(A: number[], n: number) { | ||
* @param A | ||
* @internal | ||
*/ | ||
@@ -72,2 +68,3 @@ export function per(A: number[]) { | ||
* @param B | ||
* @internal | ||
*/ | ||
@@ -79,4 +76,15 @@ export function dpr(A: number[], B: number[]) { | ||
/** | ||
* Get whether two vectors are equal. | ||
* @param A | ||
* @param B | ||
* @internal | ||
*/ | ||
export function isEqual(A: number[], B: number[]) { | ||
return A[0] === B[0] && A[1] === B[1] | ||
} | ||
/** | ||
* Length of the vector | ||
* @param A | ||
* @internal | ||
*/ | ||
@@ -90,2 +98,3 @@ export function len(A: number[]) { | ||
* @param A | ||
* @internal | ||
*/ | ||
@@ -100,2 +109,3 @@ export function len2(A: number[]) { | ||
* @param B | ||
* @internal | ||
*/ | ||
@@ -109,2 +119,3 @@ export function dist2(A: number[], B: number[]) { | ||
* @param A | ||
* @internal | ||
*/ | ||
@@ -119,2 +130,3 @@ export function uni(A: number[]) { | ||
* @param B | ||
* @internal | ||
*/ | ||
@@ -129,2 +141,3 @@ export function dist(A: number[], B: number[]) { | ||
* @param B | ||
* @internal | ||
*/ | ||
@@ -140,2 +153,3 @@ export function med(A: number[], B: number[]) { | ||
* @param r rotation in radians | ||
* @internal | ||
*/ | ||
@@ -160,20 +174,17 @@ export function rotAround(A: number[], C: number[], r: number) { | ||
* @param t scalar | ||
* @internal | ||
*/ | ||
export function lrp(A: number[], B: number[], t: number) { | ||
return add(A, mul(vec(A, B), t)) | ||
return add(A, mul(sub(B, A), t)) | ||
} | ||
// isLeft: >0 for counterclockwise | ||
// =0 for none (degenerate) | ||
// <0 for clockwise | ||
export function isLeft(p1: number[], pc: number[], p2: number[]) { | ||
return (pc[0] - p1[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (pc[1] - p1[1]) | ||
/** | ||
* Project a point A in the direction B by a scalar c | ||
* @param A | ||
* @param B | ||
* @param c | ||
* @internal | ||
*/ | ||
export function prj(A: number[], B: number[], c: number) { | ||
return add(A, mul(B, c)) | ||
} | ||
export function clockwise(p1: number[], pc: number[], p2: number[]) { | ||
return isLeft(p1, pc, p2) > 0 | ||
} | ||
export function isEqual(a: number[], b: number[]) { | ||
return a[0] === b[0] && a[1] === b[1] | ||
} |
@@ -11,3 +11,11 @@ { | ||
"dist" | ||
] | ||
], | ||
"compilerOptions": { | ||
"composite": false, | ||
"incremental": false, | ||
"declarationMap": true, | ||
"sourceMap": true, | ||
"emitDeclarationOnly": true, | ||
"stripInternal": true | ||
} | ||
} |
@@ -8,7 +8,4 @@ { | ||
"outDir": "./dist/types", | ||
"baseUrl": "src", | ||
"paths": { | ||
"+*": ["./*"] | ||
} | ||
"baseUrl": "src" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
110907
38
2782
1
1