Skip to content

Instantly share code, notes, and snippets.

@micnic
Last active September 30, 2017 12:40
Show Gist options
  • Save micnic/6aec085d63320229a778c6775ec7f9aa to your computer and use it in GitHub Desktop.
Save micnic/6aec085d63320229a778c6775ec7f9aa to your computer and use it in GitHub Desktop.
Chain Drive System
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
this.length = Math.sqrt(x * x + y * y);
}
add(vector) {
return new Vector(this.x + vector.x, this.y + vector.y);
}
subtract(vector) {
return new Vector(this.x - vector.x, this.y - vector.y);
}
multiply(scalar) {
return new Vector(this.x * scalar, this.y * scalar);
}
rotate(radians) {
const cos = Math.cos(radians);
const sin = Math.sin(radians);
return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
}
cross(vector) {
return this.x * vector.y - this.y * vector.x;
}
toString() {
return `${this.x},${this.y}`;
}
}
class Gear {
constructor(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
getVector() {
return new Vector(this.x, this.y);
}
}
const setAttributes = (element, attributes) => {
Object.keys(attributes).forEach((attribute) => {
element.setAttribute(attribute, attributes[attribute]);
});
};
const createElement = (tagName, attributes) => {
const element = document.createElementNS('http://www.w3.org/2000/svg', tagName);
setAttributes(element, attributes);
return element;
};
const cloneElement = (element, attributes) => {
const clone = element.cloneNode();
setAttributes(clone, attributes);
return clone;
};
const createPath = (attributes) => {
return createElement('path', {
...attributes,
fill: 'none'
});
};
const createCircle = (cx, cy, r, fill) => {
return createElement('circle', {
cx,
cy,
r,
fill
});
};
const loopGears = (gears, callback) => {
const length = gears.length;
gears.forEach((gear, index) => {
const prevGear = gears[(length + index - 1) % length];
const nextGear = gears[(index + 1) % length];
callback(gear, prevGear, nextGear);
});
};
const arcDescription = (radius, largeArcFlag, sweepFlag, endVector) => {
return `A${radius} ${radius} 0 ${+largeArcFlag} ${+sweepFlag} ${endVector}`;
};
const renderGears = (data) => {
let x = Infinity;
let y = Infinity;
let w = -Infinity;
let h = -Infinity;
const gears = data.map((params) => {
const gear = new Gear(...params);
const unit = params[2] + 5;
x = Math.min(x, gear.x - unit);
y = Math.min(y, gear.y - unit);
w = Math.max(w, gear.x + unit);
h = Math.max(h, gear.y + unit);
return gear;
});
const firstGear = gears[0];
w -= x;
h -= y;
const svg = createElement('svg', {
width: w,
height: h,
viewBox: `${x} ${y} ${w} ${h}`,
transform: `scale(1,-1)`
});
let chainPath = '';
loopGears(gears, (gear, prevGear, nextGear) => {
const gearVector = gear.getVector();
const prevGearVector = prevGear.getVector().subtract(gearVector);
const nextGearVector = nextGear.getVector().subtract(gearVector);
gear.sweep = (prevGearVector.cross(nextGearVector) > 0);
});
loopGears(gears, (gear, prevGear, nextGear) => {
const diffVector = gear.getVector().subtract(nextGear.getVector());
let angle = 0;
let x = 1 / diffVector.length;
let y = x;
if (gear.sweep === nextGear.sweep) {
angle = Math.asin((gear.radius - nextGear.radius) * x);
if (gear.sweep) {
x = -x;
y = -y;
angle = -angle;
}
} else {
angle = Math.asin((gear.radius + nextGear.radius) * x);
if (gear.sweep) {
x = -x;
angle = -angle;
} else {
y = -y;
}
}
const perpendicularVector = diffVector.rotate(angle + Math.PI / 2);
gear.out = perpendicularVector.multiply(x * gear.radius).add(gear.getVector());
nextGear.in = perpendicularVector.multiply(y * nextGear.radius).add(nextGear.getVector());
});
loopGears(gears, (gear, prevGear, nextGear) => {
const largeArcFlag = (prevGear.out.subtract(nextGear.in).length < gear.in.subtract(gear.out).length);
const arcPath = arcDescription(gear.radius, largeArcFlag, !gear.sweep, gear.out);
const gearExterior = createCircle(gear.x, gear.y, gear.radius - 1.5, '#888');
const gearInterior = createCircle(gear.x, gear.y, gear.radius - 4.5, '#666');
const gearCenter = createCircle(gear.x, gear.y, 3, '#888');
const gearTeeth = createPath({
d: `M${gear.in}${arcPath}${arcDescription(gear.radius, !largeArcFlag, !gear.sweep, gear.in)}`,
stroke: '#888',
'stroke-width': 5
});
const chainParts = cloneElement(gearTeeth, {
d: `M${gear.in}${arcPath}L${nextGear.in}`,
stroke: '#222'
});
gear.teeth = gearTeeth;
gear.chainParts = chainParts;
chainPath += `${arcPath}L${nextGear.in}`;
svg.appendChild(gearExterior);
svg.appendChild(gearInterior);
svg.appendChild(gearCenter);
svg.appendChild(gearTeeth);
svg.appendChild(chainParts);
});
const chain = cloneElement(firstGear.chainParts, {
d: 'M' + firstGear.in + chainPath,
'stroke-width': 2
});
const chainLength = chain.getTotalLength();
const chainUnit = chainLength / Math.round(chainLength / (4 * Math.PI)) / 2;
const animationOffset = 8 * chainUnit;
loopGears(gears, (gear, prevGear) => {
if (gear === firstGear) {
gear.teethOffset = chainUnit;
gear.chainOffset = 0;
} else {
gear.teethOffset = prevGear.teethOffset + prevGear.chainParts.getTotalLength();
gear.chainOffset = prevGear.chainOffset + prevGear.chainParts.getTotalLength();
}
setAttributes(gear.teeth, {
'stroke-dasharray': chainUnit,
'stroke-dashoffset': gear.teethOffset
});
setAttributes(gear.chainParts, {
'stroke-dasharray': chainUnit,
'stroke-dashoffset': gear.chainOffset
});
const animate = createElement('animate', {
attributeName: 'stroke-dashoffset',
from: gear.teethOffset + animationOffset,
to: gear.teethOffset,
repeatCount: 'indefinite',
dur: '1s'
});
const cloneAnimate = cloneElement(animate, {
from: gear.chainOffset + animationOffset,
to: gear.chainOffset
});
gear.teeth.appendChild(animate);
gear.chainParts.appendChild(cloneAnimate);
});
svg.appendChild(chain);
document.body.appendChild(svg);
};
R=g=>{with(Math){v='stroke';j=v+'-dasharray';q=v+'-dashoffset';m='appendChild';n='getTotalLength';b='setAttribute';z='#888';k=document;V=(x,y,r,o)=>o={x,y,r,l:sqrt(x*x+y*y),a:v=>V(x+v.x,y+v.y),s:v=>o.a(v.m(-1)),m:f=>V(x*f,y*f),t:r=>V(x*cos(r)-y*sin(r),x*sin(r)+y*cos(r)),c:v=>x*v.y-y*v.x,toString:_=>x+','+y};S=(e,a)=>Object.keys(a).map(n=>e[b](n,a[n]))&&e;T=(t,a)=>S(k.createElementNS('http://www.w3.org/2000/svg',t),a);C=(e,a)=>S(e.cloneNode(),a);w=h=-(x=y=1/0);G=g.map((a,g)=>(g=V(...a))&&(u=(g.r=a[2])+5,x=min(x,g.x-u),y=min(y,g.y-u),w=max(w,g.x+u),h=max(h,g.y+u))&&g);f=G[0];w-=x;h-=y;s=T('svg',{width:w,height:h,viewBox:x+' '+y+' '+w+' '+h,transform:'scale(1,-1)'});c='';L=(c)=>(h=G.length)&&G.map((g,i)=>c(G[i],G[(h+i-1)%h],G[(i+1)%h]))&&L;L((g,p,n)=>g.w=(p.s(g).c(n.s(g))>0))((g,p,n)=>{d=g.s(n),y=x=1/d.l;g.w!=n.w?(p=asin((g.r+n.r)*x),g.w?(x=-x,p=-p):(y=-y)):(p=asin((g.r-n.r)*x),g.w&&(x=y=-x,p=-p));t=d.t(p+PI/2);g.o=t.m(x*g.r).a(g);n.i=t.m(y*n.r).a(n)})((g,p,n)=>{l=(p.o.s(n.i).l<g.i.s(g.o).l);d=(l,e)=>`A${g.r} ${g.r} 0 ${+l} ${+!g.w} ${e}`;a=d(l,g.o);e=(f,r)=>T('circle',{cx:g.x,cy:g.y,r,fill:f});c+=a+'L'+n.i;s[m](e(z,g.r-1.5));s[m](e('#666',g.r-4.5));s[m](e(z,3));g.p=s[m](C(g.e=s[m](T('path',{d:'M'+g.i+a+d(!l,g.i),fill:'none',[v]:z,[v+'-width']:5})),{d:'M'+g.i+a+'L'+n.i,[v]:'#222'}))});c=C(f.p,{d:'M'+f.i+c,[v+'-width']:2});g=c[n]();y=8*(x=g/round(g/(4*PI))/2);f.g=x;f.h=0;L((g,p)=>{g!=f&&(g.g=p.g+p.p[n](),g.h=p.h+p.p[n]());S(g.p,{[j]:x,[q]:g.h})[m](C(S(g.e,{[j]:x,[q]:g.g})[m](T('animate',{attributeName:[q],from:g.g+y,to:g.g,repeatCount:'indefinite',dur:'1s'})),{from:g.h+y,to:g.h}))});k.body[m](s)[m](c)}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment