Hypotrochoids based on formulas in en.wikipedia.org/wiki/hypotrochoid I got to when Google threw this image at me: pinterest.com/pin/pa…-363525001150121862/
Var a, b and c can be set using the _num(erator) / _den(ominator) sliders.
In that image:
[1,7/13,1/13] Yet more spirographs ➿ (variation)
[1,7/13,5/13] Yet more spirographs ➿ (variation)
[1,7/13,7/13] Yet more spirographs ➿ (variation)
[1,7/13,15/13] Yet more spirographs ➿ (variation)
[1,7/5,1/13] Yet more spirographs ➿ (variation)
[1,7/5,5/13] Yet more spirographs ➿ (variation)
[1,7/5,7/13] Yet more spirographs ➿ (variation)
[1,7/5,15/13] Yet more spirographs ➿ (variation)
(When a drawing seems not to have smooth curves increase stepsPerT)
Log in to post a comment.
const showRecipe = 1; //min=0 max=2 step=1 (No, Yes, Yes including parametric equation)
const x = '(a-b)*Math.cos(t*2*Math.PI)+c*Math.cos(t*2*Math.PI*(a-b)/b)'; //type=string
const y = '(a-b)*Math.sin(t*2*Math.PI)-c*Math.sin(t*2*Math.PI*(a-b)/b)'; //type=string
const a_num = 11; //min=-50 max=50 step=1
const a_den = 2; //min=-50 max=50 step=1
const b_num = 5; //min=-50 max=50 step=1
const b_den = 3; //min=-50 max=50 step=1
const c_num = 5;//min=-50 max=50 step=1
const c_den = 2;//min=-50 max=50 step=1
const stepsPerT = 1000; //min=10 max=3000 step=10
const params = {
a: a_num/a_den,
b: b_num/b_den,
c: c_num/c_den,
};
function calculateTMax(a_num, a_den, b_num, b_den) {
function gcd(a, b) {
a = Math.abs(a);
b = Math.abs(b);
return b === 0 ? a : gcd(b, a % b);
}
return Math.abs(b_num * a_den) / gcd(Math.abs(a_num * b_den), Math.abs(b_num * a_den));
}
const tMin = 0;
const tMax = calculateTMax(a_num, a_den, b_num, b_den);
const steps = tMax * stepsPerT;
// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(.75);
// Global code will be evaluated once.
const turtle = new Turtle();
const fnX = new Formula(x);
const fnY = new Formula(y);
const pts = [];
const bbox = [[1000, 1000], [-1000, -1000]];
for(let i = 0; i <= steps; i++) {
pts.push([
fnX.solve({t:tMin + i*(tMax - tMin)/steps, ...params}),
fnY.solve({t:tMin + i*(tMax - tMin)/steps, ...params}),
]);
bbox[0] = [Math.min(bbox[0][0], pts[pts.length-1][0]), Math.min(bbox[0][1], pts[pts.length-1][1])];
bbox[1] = [Math.max(bbox[1][0], pts[pts.length-1][0]), Math.max(bbox[1][1], pts[pts.length-1][1])];
}
if(showRecipe > 0) {
(() => {
const writer = new Text({"name":"HersheySerifMed","unitsPerEm":1000,"ascent":800,"descent":-200,"capHeight":500,"xHeight":300,"glyphs":"IADEDgDAIQBODBgLJObYCaToGAto904MpOgYCyTmAEAYC6ToGAv87wBAGAuK\/dgJxf4YCwAATgzF\/hgLiv0AQADAIgAmFpgIJOZiB2TnYgfG7gBAmAhk52IHxu4AQJgIJObYCWTnYgfG7gBAsBMk5nASZOdwEsbuAECwE2TncBLG7gBAsBMk5vAUZOdwEsbuAEAAwCMA3BkEEDjhYgeYCABAXBc44cQOmAgAQGIHPPGcGDzxAEAsBp74XBee+ABAAMAkAJwYTgw44U4M7AQAQDoROOE6EewEAEAmFtrp8BQQ6yYWUOxcFxDrXBfa6fAUZOc6ESTmTgwk5pgIZOcsBtrpLAZQ7GIHxu6YCPzvGAs88XASsvPwFOj0XBdo9wBALAZQ7JgIxu4YC\/zvcBJ88vAUsvMmFuj0XBdo91wXT\/zwFMX+OhEAAE4MAACYCMX+LAZP\/CwGFPtiB9T5mAgU+2IHT\/wAQADAJQCIHUgcJOYsBgAAAEBODCTmxA6k6MQOEOuEDZDtGAvG7pgIxu4sBlDsLAba6WIHZOfYCSTmTgwk5sQOZOdwEqToJhak6NwZZOdIHCTmAEBcF2j38BSe+LATFPuwE4r9JhYAAJwYAAASG8X+SBxP\/Egc1PncGWj3XBdo9wBAAMAmAMgeSBz87xIbPPFIHHzyiB088Ygd\/O9IHMbuEhvG7twZ\/O+cGHzyJhae+LATT\/w6EcX+xA4AABgLAABiB8X+LAZP\/CwGnvhiByj2xA488ToRxu5wElDscBLa6ToRZOfEDiTmTgxk5xgL2ukYC1DsTgz878QOsvPwFE\/8XBfF\/hIbAABIHAAAiB3F\/ogdiv0AQBgLAACYCMX+YgdP\/GIHnviYCCj2GAuy8wBAGAtQ7E4Mxu4mFk\/8nBjF\/hIbAAAAQADAJwDYCWIHJOYsBsbuAECYCCTmLAbG7gBAAMAoADoRBBA44YQNuOMYC2TnmAhQ7GIHfPJiB2j3mAiK\/RgLdgKEDSwGBBCYCABAhA244xgLpOjYCVDsmAh88pgIaPfYCYr9GAs7AYQNLAYAQADAKQA6ESwGOOGYCLjjGAtk54QNUOzEDnzyxA5o94QNiv0YC3YCmAgsBiwGmAgAQJgIuOMYC6ToTgxQ7IQNfPKEDWj3TgyK\/RgLOwGYCCwGAEAAwCoAsBNODJDtTgxP\/ABALAY88XASnvgAQHASPPEsBp74AEAAwCsA\/h9wEtrpcBIAAABAYgfo9Igd6PQAQADALADYCZgIFPtiB0\/8LAYU+2IH1PmYCBT7mAiK\/SwGAAAAQADALQD+H2IH6PSIHej0AEAAwC4A2AliB9T5LAYU+2IHT\/yYCBT7YgfU+QBAAMAvABIbEhs44ewEmAgAQADAMACcGIQNJObYCWTnYgcQ6ywGPPEsBuj0YgcU+9gJxf6EDQAABBAAALATxf4mFhT7XBfo9FwXPPEmFhDrsBNk5wQQJOaEDSTmAECEDSTmGAtk59gJpOiYCBDrYgc88WIH6PSYCBT72AmK\/RgLxf6EDQAAAEAEEAAAcBLF\/rATiv3wFBT7Jhbo9CYWPPHwFBDrsBOk6HASZOcEECTmAEAAwDEAnBjYCRDrTgza6QQQJOYEEAAAAEDEDmTnxA4AAABA2AkAAPAUAAAAQADAMgCcGGIHEOuYCFDsYgeQ7SwGUOwsBhDrYgek6JgIZOdODCTmOhEk5vAUZOcmFqToXBcQ61wXkO0mFvzvcBJ88k4M6PTYCSj2Ygee+CwGT\/wsBgAAAEA6ESTmsBNk5\/AUpOgmFhDrJhaQ7fAU\/O86EXzyTgzo9ABALAaK\/WIHT\/zYCU\/8BBDF\/rATxf4mFor9XBdP\/ABA2AlP\/AQQAADwFAAAJhbF\/lwXT\/xcF9T5AEAAwDMAnBhiBxDrmAhQ7GIHkO0sBlDsLAYQ62IHpOiYCGTnTgwk5joRJObwFGTnJhba6SYWkO3wFPzvOhE88YQNPPEAQDoRJOawE2Tn8BTa6fAUkO2wE\/zvOhE88QBAOhE88bATfPImFuj0XBdo91wXFPsmFor98BTF\/joRAABODAAAmAjF\/mIHiv0sBhT7LAbU+WIHnviYCNT5YgcU+wBA8BSy8yYWaPcmFhT78BSK\/bATxf46EQAAAEAAwDQAnBg6EaToOhEAAABAcBIk5nASAAAAQHASJObsBJ74nBie+ABAhA0AACYWAAAAQADANQCcGJgIJOYsBnzyAEAsBnzymAj8704Mxu4EEMbusBP87yYWfPJcFyj2XBee+CYWT\/ywE8X+BBAAAE4MAACYCMX+YgeK\/SwGFPssBtT5Ygee+JgI1PliBxT7AEAEEMbucBL87\/AUfPImFij2Jhae+PAUT\/xwEsX+BBAAAABAmAgk5vAUJOYAQJgIZOfEDmTn8BQk5gBAAMA2AJwY8BTa6bATEOvwFFDsJhYQ6yYW2unwFGTncBIk5sQOJOYYC2TnmAja6WIHUOwsBjzxLAae+GIHT\/zYCcX+hA0AAAQQAACwE8X+JhZP\/FwXnvhcF2j3Jhay87ATPPEEEPzvxA787xgLPPGYCLLzYgdo9wBAxA4k5k4MZOfYCdrpmAhQ7GIHPPFiB574mAhP\/BgLxf6EDQAAAEAEEAAAcBLF\/vAUT\/wmFp74JhZo9\/AUsvNwEjzxBBD87wBAAMA3AJwYLAYk5iwGkO0AQCwGEOtiB6To2Akk5k4MJOZwEtrp8BTa6SYWpOhcFyTmAEBiB6To2Alk504MZOdwEtrpAEBcFyTmXBfa6SYWkO06EbLzBBAo9sQO1PnEDgAAAEAmFpDtBBCy88QOKPaEDdT5hA0AAABAAMA4AJwYTgwk5pgIZOdiB9rpYgeQ7ZgI\/O9ODDzxOhE88fAU\/O8mFpDtJhba6fAUZOc6ESTmTgwk5gBATgwk5tgJZOeYCNrpmAiQ7dgJ\/O9ODDzxAEA6ETzxsBP87\/AUkO3wFNrpsBNk5zoRJOYAQE4MPPGYCHzyYgey8ywGKPYsBhT7YgeK\/ZgIxf5ODAAAOhEAAPAUxf4mFor9XBcU+1wXKPYmFrLz8BR88joRPPEAQE4MPPHYCXzymAiy82IHKPZiBxT7mAiK\/dgJxf5ODAAAAEA6EQAAsBPF\/vAUiv0mFhT7JhYo9vAUsvOwE3zyOhE88QBAAMA5AJwYJhbG7vAUfPJwEuj0xA4o9oQNKPbYCej0Ygd88iwGxu4sBpDtYgfa6dgJZOeEDSTmBBAk5rATZOcmFtrpXBeQ7VwX6PQmFtT58BRP\/HASxf7EDgAAGAsAAJgIxf5iB0\/8YgcU+5gI1PnYCRT7mAhP\/ABAhA0o9hgL6PSYCHzyYgfG7mIHkO2YCNrpGAtk54QNJOYAQAQQJOZwEmTn8BTa6SYWkO0mFuj08BTU+bATT\/w6EcX+xA4AAABAAMA6ANgJYgc88SwGfPJiB7LzmAh88mIHPPEAQGIH1PksBhT7YgdP\/JgIFPtiB9T5AEAAwDsA2AliBzzxLAZ88mIHsvOYCHzyYgc88QBAmAgU+2IHT\/wsBhT7YgfU+ZgIFPuYCIr9LAYAAABAAMA8AIgdEhva6WIH6PQSGwAAAEAAwD0A\/h9iBzzxiB088QBAYgee+IgdnvgAQADAPgCIHWIH2ukSG+j0YgcAAABAAMA\/ACYWYgcQ65gIUOxiB5DtLAZQ7CwGEOtiB6TomAhk5xgLJObEDiTmcBJk57ATpOjwFBDr8BSQ7bAT\/O9wEjzxhA2y84QNaPcAQMQOJOY6EWTncBKk6LATEOuwE5DtcBL87wQQfPIAQIQNiv1ODMX+hA0AAMQOxf6EDYr9AEAAwEAANCGcGPzvXBeQ7fAUUOw6EVDsxA6Q7YQNxu5ODHzyTgwo9oQNnvgEENT5sBPU+SYWnvhcFyj2AEA6EVDsxA7G7oQNfPKEDSj2xA6e+AQQ1PkAQJwYUOxcFyj2XBee+NwZ1PlIHNT5yB5o9\/4fsvP+HzzxyB6Q7YgdEOsSG6TonBhk5\/AUJOY6ESTmhA1k5xgLpOiYCBDrYgeQ7SwGPPEsBuj0Ygee+JgIFPsYC4r9hA3F\/joRAADwFAAAnBjF\/hIbiv1IHE\/8AEDcGVDsnBgo9pwYnvjcGdT5AEAAwEEAnBjEDiTmLAYAAABAxA4k5lwXAAAAQMQO2ukmFgAAAECYCJ74sBOe+ABAsQMAABgLAAAAQHASAADcGQAAAEAAwEIAEhuYCCTmmAgAAABA2Akk5tgJAAAAQOwEJOawEyTmXBdk55wYpOjcGRDr3BmQ7ZwY\/O9cFzzxsBN88gBAsBMk5iYWZOdcF6TonBgQ65wYkO1cF\/zvJhY88bATfPIAQNgJfPKwE3zyXBey85wY6PTcGWj33BkU+5wYiv1cF8X+sBMAAOwEAAAAQLATfPImFrLzXBfo9JwYaPecGBT7XBeK\/SYWxf6wEwAAAEAAwEMA3BlcF9rpnBiQ7ZwYJOZcF9rp8BRk5zoRJObEDiTmGAtk55gI2uliB1DsLAb87ywGKPZiB9T5mAhP\/BgLxf7EDgAAOhEAAPAUxf5cF0\/8nBjU+QBAxA4k5k4MZOfYCdrpmAhQ7GIH\/O9iByj2mAjU+dgJT\/xODMX+xA4AAABAAMBEABIbmAgk5pgIAAAAQNgJJObYCQAAAEDsBCTmOhEk5vAUZOdcF9rpnBhQ7NwZ\/O\/cGSj2nBjU+VwXT\/zwFMX+OhEAAOwEAAAAQDoRJOawE2TnJhba6VwXUOycGPzvnBgo9lwX1PkmFk\/8sBPF\/joRAAAAQADARQDcGZgIJOaYCAAAAEDYCSTm2AkAAABAOhGQ7ToRaPcAQOwEJOacGCTmnBiQ7VwXJOYAQNgJfPI6EXzyAEDsBAAAnBgAAJwYnvhcFwAAAEAAwEYAnBiYCCTmmAgAAABA2Akk5tgJAAAAQDoRkO06EWj3AEDsBCTmnBgk5pwYkO1cFyTmAEDYCXzyOhF88gBA7AQAAIQNAAAAQADARwBIHFwX2umcGJDtnBgk5lwX2unwFGTnOhEk5sQOJOYYC2TnmAja6WIHUOwsBvzvLAYo9mIH1PmYCE\/8GAvF\/sQOAAA6EQAA8BTF\/lwXT\/wAQMQOJOZODGTn2Ana6ZgIUOxiB\/zvYgco9pgI1PnYCU\/8TgzF\/sQOAAAAQFwXKPZcFwAAAECcGCj2nBgAAABAsBMo9kgcKPYAQADASACIHZgIJOaYCAAAAEDYCSTm2AkAAABAnBgk5pwYAAAAQNwZJObcGQAAAEDsBCTmhA0k5gBA8BQk5ogdJOYAQNgJfPKcGHzyAEDsBAAAhA0AAABA8BQAAIgdAAAAQADASQCEDZgIJOaYCAAAAEDYCSTm2AkAAABA7AQk5oQNJOYAQOwEAACEDQAAAEAAwEoAcBLEDiTmxA4U+4QNxf4YCwAAmAgAACwGxf7sBE\/87ATU+SwGnvhiB9T5LAYU+wBAhA0k5oQNFPtODMX+GAsAAABA2Akk5nASJOYAQADASwASG5gIJOaYCAAAAEDYCSTm2AkAAABA3Bkk5tgJKPYAQAQQPPHcGQAAAEDEDjzxnBgAAABA7AQk5oQNJOYAQPAUJOZIHCTmAEDsBAAAhA0AAABA8BQAAEgcAAAAQADATAAmFpgIJOaYCAAAAEDYCSTm2AkAAABA7AQk5oQNJOYAQOwEAABcFwAAXBee+CYWAAAAQADATQDIHpgIJOaYCAAAAEDYCSTmOhFP\/ABAmAgk5joRAAAAQNwZJOY6EQAAAEDcGSTm3BkAAABAEhsk5hIbAAAAQOwEJObYCSTmAEDcGSTmyB4k5gBA7AQAAE4MAAAAQCYWAADIHgAAAEAAwE4ASByYCCTmmAgAAABA2Akk5pwYiv0AQNgJpOicGAAAAECcGCTmnBgAAABA7AQk5tgJJOYAQPAUJOZIHCTmAEDsBAAATgwAAABAAMBPABIbxA4k5hgLZOeYCNrpYgdQ7CwGPPEsBuj0YgfU+ZgIT\/wYC8X+xA4AADoRAADwFMX+XBdP\/JwY1PncGej03Bk88ZwYUOxcF9rp8BRk5zoRJObEDiTmAEDEDiTmTgxk59gJ2umYCFDsYgc88WIH6PSYCNT52AlP\/E4Mxf7EDgAAAEA6EQAAsBPF\/iYWT\/xcF9T5nBjo9JwYPPFcF1DsJhba6bATZOc6ESTmAEAAwFAAEhuYCCTmmAgAAABA2Akk5tgJAAAAQOwEJOawEyTmXBdk55wYpOjcGRDr3BnG7pwYPPFcF3zysBOy89gJsvMAQLATJOYmFmTnXBek6JwYEOucGMbuXBc88SYWfPKwE7LzAEDsBAAAhA0AAABAAMBRABIbxA4k5hgLZOeYCNrpYgdQ7CwGPPEsBuj0YgfU+ZgIT\/wYC8X+xA4AADoRAADwFMX+XBdP\/JwY1PncGej03Bk88ZwYUOxcF9rp8BRk5zoRJObEDiTmAEDEDiTmTgxk59gJ2umYCFDsYgc88WIH6PSYCNT52AlP\/E4Mxf7EDgAAAEA6EQAAsBPF\/iYWT\/xcF9T5nBjo9JwYPPFcF1DsJhba6bATZOc6ESTmAEAYC4r9GAtP\/E4M1PnEDp74BBCe+HAS1PmwE0\/88BTsBCYWLAacGCwG3BmxA9wZdgIAQLATT\/zwFDsBJhaxA1wX7AScGOwE3BmxAwBAAMBSABIbmAgk5pgIAAAAQNgJJObYCQAAAEDsBCTmsBMk5lwXZOecGKTo3BkQ69wZkO2cGPzvXBc88bATfPLYCXzyAECwEyTmJhZk51wXpOicGBDrnBiQ7VwX\/O8mFjzxsBN88gBA7AQAAIQNAAAAQAQQfPJwErLzsBPo9FwXiv2cGMX+3BnF\/hIbiv0AQHASsvOwEyj2JhbF\/lwXAADcGQAAEhuK\/RIbT\/wAQADAUwCcGCYW2ulcFyTmXBeQ7SYW2umwE2TnBBAk5k4MJOaYCGTnLAba6SwGUOxiB8bumAj87xgLPPFwErLz8BTo9FwXaPcAQCwGUOyYCMbuGAv873ASfPLwFLLzJhbo9FwXaPdcF0\/88BTF\/joRAACEDQAA2AnF\/mIHT\/wsBp74LAYAAGIHT\/wAQADAVABcF4QNJOaEDQAAAEDEDiTmxA4AAABALAYk5uwEkO3sBCTmXBck5lwXkO0mFiTmAEDYCQAAcBIAAABAAMBVAIgdmAgk5pgInvjYCU\/8TgzF\/gQQAABwEgAAJhbF\/pwYT\/zcGZ743Bkk5gBA2Akk5tgJnvgYC0\/8hA3F\/gQQAAAAQOwEJOaEDSTmAEAmFiTmiB0k5gBAAMBWAJwYLAYk5sQOAAAAQGIHJObEDk\/8AEBcFyTmxA4AAABAsQMk5hgLJOYAQHASJObcGSTmAEAAwFcAiB1iByTmTgwAAABAmAgk5k4M1PkAQDoRJOZODAAAAEA6ESTmJhYAAABAcBIk5iYW1PkAQBIbJOYmFgAAAECxAyTmTgwk5gBAXBck5sgeJOYAQADAWACcGCwGJOYmFgAAAEBiByTmXBcAAABAXBck5iwGAAAAQLEDJOYYCyTmAEBwEiTm3Bkk5gBAsQMAABgLAAAAQHASAADcGQAAAEAAwFkA3BksBiTmxA6y88QOAAAAQGIHJOYEELLzBBAAAABAnBgk5gQQsvMAQLEDJOYYCyTmAECwEyTmEhsk5gBAGAsAALATAAAAQADAWgCcGCYWJOYsBgAAAEBcFyTmYgcAAABAYgck5iwGkO0sBiTmXBck5gBALAYAAFwXAABcF574JhYAAABAAMBbADoRYgc44WIHmAgAQJgIOOGYCJgIAEBiBzjhBBA44QBAYgeYCAQQmAgAQADAXAA6EXYCJOawE7EDAEAAwF0AOhGEDTjhhA2YCABAxA444cQOmAgAQCwGOOHEDjjhAEAsBpgIxA6YCABAAMBeABIbLAZo9wQQPPHcGWj3AEAsBmj3BBB88twZaPcAQADAXwCcGHYCmAgSG5gIAEAAwGAAxA5iByTmhA2Q7QBAYgck5iwGZOeEDZDtAEAAwGEAnBiYCDzxmAh88mIHfPJiBzzxmAj87xgLxu4EEMbucBL877ATPPHwFLLz8BRP\/CYWxf5cFwAAAECwEzzxsBNP\/PAUxf5cFwAAnBgAAABAsBOy83AS6PQYCyj2Ygdo9ywG1PksBk\/8YgfF\/hgLAADEDgAAOhHF\/rATT\/wAQBgLKPaYCGj3YgfU+WIHT\/yYCMX+GAsAAABAAMBiANwZmAgk5pgIAAAAQNgJJObYCQAAAEDYCXzyTgz878QOxu46Ecbu8BT871wXfPKcGCj2nBie+FwXT\/zwFMX+OhEAAMQOAABODMX+2AlP\/ABAOhHG7rAT\/O8mFnzyXBco9lwXnvgmFk\/8sBPF\/joRAAAAQOwEJObYCSTmAEAAwGMAXBfwFHzysBOy8\/AU6PQmFrLzJhZ88rAT\/O86EcbuhA3G7tgJ\/O9iB3zyLAYo9iwGnvhiB0\/82AnF\/oQNAAAEEAAAsBPF\/iYWT\/wAQIQNxu4YC\/zvmAh88mIHKPZiB574mAhP\/BgLxf6EDQAAAEAAwGQA3BnwFCTm8BQAAABAJhYk5iYWAAAAQPAUfPJwEvzvBBDG7oQNxu7YCfzvYgd88iwGKPYsBp74YgdP\/NgJxf6EDQAABBAAAHASxf7wFE\/8AECEDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AAABAOhEk5iYWJOYAQPAUAADcGQAAAEAAwGUAXBdiByj2JhYo9iYWsvPwFDzxsBP87zoRxu6EDcbu2An872IHfPIsBij2LAae+GIHT\/zYCcX+hA0AAAQQAACwE8X+JhZP\/ABA8BQo9vAUfPKwE\/zvAECEDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AAABAAMBmAAQQxA5k54QNpOjEDtrpBBCk6AQQZOfEDiTmTgwk5tgJZOeYCNrpmAgAAABATgwk5hgLZOfYCdrp2AkAAABA7ATG7sQOxu4AQOwEAACEDQAAAEAAwGcAXBdODMbu2An875gIPPFiB7LzYgco9pgInvjYCdT5TgwU+8QOFPs6EdT5cBKe+LATKPawE7LzcBI88ToR\/O\/EDsbuTgzG7gBA2An875gIfPKYCGj32AnU+QBAOhHU+XASaPdwEnzyOhH87wBAcBI88bAT\/O8mFsbuJhb877AT\/O8AQJgInvhiB9T5LAZP\/CwGiv1iBwAAGAs7AToROwHwFHYCJhaxAwBALAaK\/WIHxf4YCwAAOhEAAPAUOwEmFrEDJhbsBPAUYgc6EZgI2AmYCCwGYgfsBOwE7ASxAywGOwHYCQAAAEAAwGgAEhuYCCTmmAgAAABA2Akk5tgJAAAAQNgJfPJODPzvBBDG7nASxu4mFvzvXBd88lwXAAAAQHASxu7wFPzvJhZ88iYWAAAAQOwEJObYCSTmAEDsBAAAhA0AAABAcBIAABIbAAAAQADAaQCEDZgIJOZiB2TnmAik6NgJZOeYCCTmAECYCMbumAgAAABA2AnG7tgJAAAAQOwExu7YCcbuAEDsBAAAhA0AAABAAMBqAIQN2Akk5pgIZOfYCaToGAtk59gJJOYAQBgLxu4YC+wE2AliB2IHmAjsBJgIsQNiB7EDLAbsBOwELAYsBuwEYgcAQNgJxu7YCewEmAhiB2IHmAgAQCwGxu4YC8buAEAAwGsA3BmYCCTmmAgAAABA2Akk5tgJAAAAQCYWxu7YCRT7AEAEECj2XBcAAABAxA4o9iYWAAAAQOwEJObYCSTmAEBwEsbu3BnG7gBA7AQAAIQNAAAAQHASAADcGQAAAEAAwGwAhA2YCCTmmAgAAABA2Akk5tgJAAAAQOwEJObYCSTmAEDsBAAAhA0AAABAAMBtAJYomAjG7pgIAAAAQNgJxu7YCQAAAEDYCXzyTgz87wQQxu5wEsbuJhb871wXfPJcFwAAAEBwEsbu8BT87yYWfPImFgAAAEBcF3zy3Bn874gdxu7+H8butCP87+okfPLqJAAAAED+H8budCL877QjfPK0IwAAAEDsBMbu2AnG7gBA7AQAAIQNAAAAQHASAAASGwAAAED+HwAAmygAAABAAMBuABIbmAjG7pgIAAAAQNgJxu7YCQAAAEDYCXzyTgz87wQQxu5wEsbuJhb871wXfPJcFwAAAEBwEsbu8BT87yYWfPImFgAAAEDsBMbu2AnG7gBA7AQAAIQNAAAAQHASAAASGwAAAEAAwG8AnBiEDcbu2An872IHfPIsBij2LAae+GIHT\/zYCcX+hA0AAAQQAACwE8X+JhZP\/FwXnvhcFyj2JhZ88rAT\/O8EEMbuhA3G7gBAhA3G7hgL\/O+YCHzyYgco9mIHnviYCE\/8GAvF\/oQNAAAAQAQQAABwEsX+8BRP\/CYWnvgmFij28BR88nAS\/O8EEMbuAEAAwHAA3BmYCMbumAiYCABA2AnG7tgJmAgAQNgJfPJODPzvxA7G7joRxu7wFPzvXBd88pwYKPacGJ74XBdP\/PAUxf46EQAAxA4AAE4Mxf7YCU\/8AEA6EcbusBP87yYWfPJcFyj2XBee+CYWT\/ywE8X+OhEAAABA7ATG7tgJxu4AQOwEmAiEDZgIAEAAwHEAnBjwFMbu8BSYCABAJhbG7iYWmAgAQPAUfPJwEvzvBBDG7oQNxu7YCfzvYgd88iwGKPYsBp74YgdP\/NgJxf6EDQAABBAAAHASxf7wFE\/8AECEDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AAABAOhGYCNwZmAgAQADAcgDwFJgIxu6YCAAAAEDYCcbu2AkAAABA2Ako9hgLfPKEDfzvBBDG7rATxu7wFPzv8BQ88bATfPJwEjzxsBP87wBA7ATG7tgJxu4AQOwEAACEDQAAAEAAwHMA8BRwEjzxsBPG7rATsvNwEjzxOhH878QOxu7YCcbuYgf87ywGPPEsBrLzYgfo9NgJKPYEEJ74cBLU+bATFPsAQCwGfPJiB7Lz2Ano9AQQaPdwEp74sBPU+bATiv1wEsX+BBAAABgLAACYCMX+YgeK\/SwGFPssBgAAYgeK\/QBAAMB0AHASmAgk5pgIFPvYCcX+TgwAAMQOAAA6EcX+cBJP\/ABA2Akk5tgJFPsYC8X+TgwAAABA7ATG7sQOxu4AQADAdQASG5gIxu6YCE\/82AnF\/oQNAAAEEAAAsBPF\/iYWT\/wAQNgJxu7YCU\/8GAvF\/oQNAAAAQCYWxu4mFgAAAEBcF8buXBcAAABA7ATG7tgJxu4AQHASxu5cF8buAEAmFgAAEhsAAABAAMB2ACYWLAbG7oQNAAAAQGIHxu6EDYr9AEDwFMbuhA0AAABAsQPG7hgLxu4AQAQQxu5cF8buAEAAwHcAiB1iB8buTgwAAABAmAjG7k4MT\/wAQDoRxu5ODAAAAEA6EcbuJhYAAABAcBLG7iYWT\/wAQBIbxu4mFgAAAECxA8buTgzG7gBAXBfG7sgexu4AQADAeACcGGIHxu7wFAAAAECYCMbuJhYAAABAJhbG7mIHAAAAQOwExu5ODMbuAEA6EcbunBjG7gBA7AQAAE4MAAAAQDoRAACcGAAAAEAAwHkAXBdiB8buxA4AAABAmAjG7sQOiv0AQCYWxu7EDgAATgzsBNgJYgdiB5gILAaYCOwEYgcsBiwGYgdiBwBA7ATG7k4Mxu4AQDoRxu6cGMbuAEAAwHoAJhawE8buLAYAAABA8BTG7mIHAAAAQGIHxu4sBrLzLAbG7vAUxu4AQCwGAADwFAAA8BQU+7ATAAAAQADAewA6EYQNOOEYC3ji2Am445gIJOaYCKTo2AkQ6xgLUOxODMbuTgw88dgJsvMAQBgLeOLYCe7k2Alk5xgL2ulODBDrhA2Q7YQN\/O9ODHzyYgfo9E4MaPeEDdT5hA1P\/E4Mxf4YCwAA2Al2AtgJ7AQYC2IHAEDYCSj2Tgye+E4MFPsYC4r92AnF\/pgIOwGYCLED2AksBhgLYgeEDZgIAEAAwHwA2AliBzjhYgeYCABAAMB9ADoRmAg44RgLeOJODLjjhA0k5oQNpOhODBDrGAtQ7NgJxu7YCTzxTgyy8wBAGAt44k4M7uRODGTnGAva6dgJEOuYCJDtmAj879gJfPLEDuj02Alo95gI1PmYCE\/82AnF\/hgLAABODHYCTgzsBBgLYgcAQE4MKPbYCZ742AkU+xgLiv1ODMX+hA07AYQNsQNODCwGGAtiB5gImAgAQADAfgCIHSwGnvgsBij2Ygd88tgJPPFODDzxxA588rATKPYmFmj3nBho9xIbKPZIHLLzAEAsBij2Ygey89gJfPJODHzyxA6y87ATaPcmFp74nBie+BIbaPdIHLLzSBw88QBAAMA="});
if(showRecipe == 2) {
const fnXText = `${x.replace(/Math\.E/g, 'e').replace(/Math\./g, '').replace(/([^\d])(\.\d+)/g, '$10$2')}`;
const fnYText = `${y.replace(/Math\.E/g, 'e').replace(/Math\./g, '').replace(/([^\d])(\.\d+)/g, '$10$2')}`;
turtle.jump(-95, 87);
writer.print(turtle, `x = ${fnXText}`, .375);
turtle.jump(-95, 92);
writer.print(turtle, `y = ${fnYText}`, .375);
turtle.jump(-99, 91.2);
writer.print(turtle, '{', .9);
}
function getNumDenLabel (num, den) {
return num/den == ((num/den)|0)? `${num/den}`: `${num*den<0?'-':''}${Math.abs(num)}/${Math.abs(den)}`;
}
turtle.jump(-95, 97);
const lines = [`0 <= t <= ${tMax}`, `a = ${getNumDenLabel(a_num,a_den)}`, `b = ${getNumDenLabel(b_num,b_den)}`, `c = ${getNumDenLabel(c_num,c_den)}`];
writer.print(turtle, lines.join(' '), .375);
})();
}
const space = 190 - showRecipe * 10;
const t = (()=> {
const height = bbox[1][1] - bbox[0][1];
const width = bbox[1][0] - bbox[0][0];
const scale = space / Math.max(height, width);
const center = [bbox[0][0] + width/2, bbox[0][1] + height/2];
return (pt) => [scale*(pt[0] - center[0]), scale*(pt[1] - center[1]) - (showRecipe == 2? 7: 0)];
})();
// The walk function will be called until it returns false.
function walk(i) {
turtle[i==0?'jump': 'goto'](t(pts[i]));
return i < pts.length - 1;
}
////////////////////////////////////////////////////////////////
// Text utility code. Created by Reinder Nijhoff 2024
// https://turtletoy.net/turtle/0f84fd3ae4
// Modifications by Jurgen Westerhof 2024: https://turtletoy.net/turtle/9aef87b45e
// - Does not force turtle to operate in radians
// - Now respects turtle.isdown() and doesn't print if true
// - Text.size(turtle, str, scale = 1) returns properties of text str to be printed
// - Text.bbox(turtle, str, scale = 1) returns bounding box of text str to be printed
// - Text.dimensions(scale = 1) returns properties of fontData for scale
// - Text.bboxPrint(turtle, str, scale = 1) prints the text and returns the absolute bounding box
////////////////////////////////////////////////////////////////
function Text(fontData) {
const decode = (data) => {
const b = atob(data), a = new Int16Array(b.length / 2), g = {};
for (let i = 0; i < b.length; i+=2) {
a[i / 2 | 0] = b.charCodeAt(i) | (b.charCodeAt(i + 1) << 8);
}
for (let i = 0;i < a.length; i++) {
let u = String.fromCharCode(a[i++]), x = a[i++], d = [], p = [];
for (;a[i] !== -16384; i++) a[i] === 16384 ? (d.push(p), p = []) : p.push(a[i]);
g[u] = { x, d };
}
return g;
}
const rotAdd = (a, b, h) => [Math.cos(h)*a[0] - Math.sin(h)*a[1] + b[0],
Math.cos(h)*a[1] + Math.sin(h)*a[0] + b[1]];
const data = Object.fromEntries(Object.entries(fontData).map(([key, value]) =>
[key, key === 'glyphs' ? decode(value) : value]));
class RangeFinderTurtle extends Turtle {
constructor(x, y) {super(x, y);this.reset();}
goto(x, y) {super.goto(x, y);const [a, b] = this.pos();this.rangeX = [Math.min(a, this.rangeX? this.rangeX[0]: a), Math.max(a, this.rangeX? this.rangeX[1]: a)];this.rangeY = [Math.min(b, this.rangeY? this.rangeY[0]: b), Math.max(b, this.rangeY? this.rangeY[1]: b)];}
reset() {const [a, b] = this.pos();this.rangeX = [a, a];this.rangeY = [b, b];}
bbox() { return [[this.rangeX[0], this.rangeY[0]], [this.rangeX[1], this.rangeY[1]]]; }
}
class Text {
print (t, str, scale = 1) {
const isDown = t && t.isdown();
let pos = t ? [t.x(), t.y()] : [0,0], h = t ? t.h() * 2*Math.PI/t.fullCircle() : 0, o = pos, s = scale / data.unitsPerEm;
str.split('').map(c => {
if (c == '\n') {
pos = o = rotAdd([0, 10 * scale], o, h);
return;
}
const glyph = (data.glyphs[c] || data.glyphs[' ']), d = glyph.d;
d.forEach( (p, k) => {
t && t.up();
for (let i=0; i<p.length; i+=2) {
t && t.goto(rotAdd([p[i]*s, p[i+1]*s], pos, h));
t && (!isDown || t.down());
}
});
pos = rotAdd([glyph.x*s, 0], pos, h);
});
if(isDown) t.down();
return rotAdd([0, 10*scale], pos, h);
}
bboxPrint(t, str, scale = 1) {
const rft = new RangeFinderTurtle(t.pos());
rft.degrees(t.fullCircle());
rft.seth(t.h());
rft.up();
this.print(rft, str, scale);
this.print(t, str, scale);
return rft.bbox();
}
size(t, str, scale = 1) {
const rft = new RangeFinderTurtle();
rft.up();
this.print(rft, str, scale);
return {
width: rft.rangeX[1] - rft.rangeX[0],
height: rft.rangeY[1] - rft.rangeY[0],
bbox: t.h() === 0? rft.bbox(): this.bbox(t, str, scale)
}
}
bbox(t, str, scale = 1) {
const rft = new RangeFinderTurtle();
rft.degrees(t.fullCircle());
rft.seth(t.h());
rft.up();
this.print(rft, str, scale);
return rft.bbox();
}
dimensions(scale = 1, standardCharacter = 'x') {
const t = new Turtle();
const x = this.size(t, standardCharacter, scale);
const allLower = this.size(t, 'abcdefghijklmnopqrstuvwxyz', scale);
const allUpper = this.size(t, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', scale);
// property names from: https://www.onlineprinters.co.uk/magazine/font-sizes/
return {
height: x.height,
width: x.width,
ascenders: Math.abs(allLower.bbox[0][1]) - x.height,
descenders: Math.abs(allLower.bbox[1][1]),
capitalHeight: Math.abs(allUpper.bbox[0][1]),
capitalDescenders: Math.abs(allUpper.bbox[1][1])
};
}
}
return new Text();
}
/////////////////////////////////////////////////////////////////
// Formula parser and solver - Created by Jurgen Westerhof 2024
// https://turtletoy.net/turtle/187a81ec7d
/////////////////////////////////////////////////////////////////
function Formula(string) {
const types = {
'Function': 'Function',
'Literal': 'Literal',
'Variable': 'Variable',
'Arithmetic': 'Arithmetic',
'Unary': 'Unary',
};
const operators = [ //ordered by operator precedence (PEMDAS)
['**', (a, b) => a**b],
['*', (a, b) => a*b],
['/', (a, b) => a/b],
['%', (a, b) => a%b],
['+', (a, b) => a+b],
['-', (a, b) => a-b],
['<<', (a, b) => a<<b],
['>>', (a, b) => a>>b],
['|', (a, b) => a|b],
['^', (a, b) => a^b],
['&', (a, b) => a&b],
];
class Formula {
#variables;
#parsed;
#raw;
#ready;
constructor(string) {
this.#raw = string;
this.#variables = [];
this.#parsed = this.tokenize(string);
}
getVariables() {
return this.#variables.map(e => e);
}
getParsed() {
const clone = (v) => (typeof v == 'object')? v.map(vv => clone(vv)): v;
return this.#parsed.map(v => clone(v));
}
tokenize(str) {
const tokens = [];
let m;
nextToken: for(let i = 0; i < str.length; i++) {
//Skip whitespace
if(/\s/.test(str[i])) continue;
//Parse Math namespace
if(str.substr(i, 5) == 'Math.') {
i += 5;
m = new RegExp(`^.{${i}}(?<payload>(?<const>[A-Z][A-Z0-9]*)|(?<fn>[a-z][a-z0-9]*)).*?`).exec(str);
if(Math[m.groups.payload] === undefined) {
console.error(`Math.${m.groups.payload} is undefined`);
}
if(m.groups.const) {
tokens.push([types.Literal, Math[m.groups.payload]]);
} else {
tokens.push([types.Function, m.groups.payload]);
}
i+=m.groups.payload.length-1;
continue nextToken;
}
//Parse variable
// taking a shortcut here: unicode in variable names not accepted: https://stackoverflow.com/questions/1661197/what-characters-are-valid-for-javascript-variable-names
m = new RegExp('^' + '\.'.repeat(i) + '(?<payload>[a-zA-Z_$][0-9a-zA-Z_$]*).*').exec(str);
if(m !== null) {
tokens.push([types.Variable, m.groups.payload]);
if(!this.#variables.includes(m.groups.payload)) {
this.#variables.push(m.groups.payload);
}
i+= m.groups.payload.length - 1;
continue nextToken;
}
//Parse unary
if((tokens.length == 0 || tokens[tokens.length - 1][0] == types.Arithmetic) && (str[i] == '-' || str[i] == '+' || str[i] == '~')) {
tokens.push([types.Unary, str[i]]);
continue nextToken;
}
//Parse (group) (including function parameters)
if(str[i] == '(') {
const isFunction = (tokens.length > 0 && tokens[tokens.length - 1][0] == types.Function);
let cnt = 1;
let k = i + 1;
let j = 0;
let fnArgs = [];
for(; 0 < cnt && k+j < str.length; j++) {
if(str[k+j] == '(') cnt++;
if(str[k+j] == ')') cnt--;
if(str[k+j] == ',' && cnt == 1) {
fnArgs.push(this.tokenize(str.substr(i+1, j)));
i += j+1;
k += j+1;
j=0;
}
}
if(cnt == 0) {
if(isFunction) {
fnArgs.push(this.tokenize(str.substr(i+1, j-1)));
tokens[tokens.length - 1].push(fnArgs)
} else {
tokens.push(this.tokenize(str.substr(i+1, j-1)));
}
i += j;
continue nextToken;
}
console.error(`Opened bracket at character ${i} not closed: ${str.substr(i)}`);
throw new Error(`Opened bracket at character ${i} not closed: ${str.substr(i)}`);
}
//Parse literal
m = new RegExp(`^.{${i}}(?<payload>\\d+(\\.\\d+)?|\\.\\d+).*?`).exec(str);
if(m !== null) {
tokens.push([types.Literal, +m.groups.payload]);
i+=m.groups.payload.length-1;
continue nextToken;
}
//Parse operator
m = new RegExp(`^.{${i}}(?<payload>\\${operators.map(o => o[0].split('').join('\\')).join('|\\')})`).exec(str);
if(m !== null) {
tokens.push([types.Arithmetic, m.groups.payload]);
i+=m.groups.payload.length-1;
continue nextToken;
}
//Something I didn't think of occured
console.error(`Unable to parse '${str}' because a character at ${i}: ${str[i]}`);
throw new Error(`Unable to parse '${str}' because a character at ${i}: ${str[i]}`);
}
return tokens;
}
solve(variableMap = {}) {
return this.solveInt(
this.#parsed,
//Map required variables to values assuming 0 if not set
this.#variables.reduce((a, c) => {
let val = 0;
if(variableMap[c] === undefined) {
console.warn(`Variable ${c} not set in argument to solve() of ${this.#raw}.`);
throw new Error(`Variable ${c} not set in argument to solve() of ${this.#raw}.`);
} else {
val = variableMap[c];
}
return {...a, [c]: val};
}, {})
);
}
solveInt(tokenss, variableMap) {
if(tokenss.length == 0) return 0;
const clone = (v) => (typeof v == 'object')? v.map(vv => clone(vv)): v;
const tokens = tokenss.map(v => clone(v));
//Resolve functions
for(let i = 0; i < tokens.length; i++) {
if(tokens[i][0] == types.Function) {
const literals = tokens[i][2].map(v => this.solveInt(v, variableMap));
tokens[i] = [types.Literal, Math[tokens[i][1]].apply(null, literals)];
}
}
//Resolve (group)s
for(let i = 0; i < tokens.length; i++) {
if(typeof tokens[i][0] == 'object') {
tokens[i] = [types.Literal, this.solveInt(tokens[i], variableMap)];
}
if(tokens[i][0] == types.Variable) {
tokens[i] = [types.Literal, typeof variableMap[tokens[i][1]] == 'function'? variableMap[tokens[i][1]](variableMap): variableMap[tokens[i][1]]];
}
}
//Resolve unary
for(let i = 0; i < tokens.length; i++) {
if(tokens[i][0] == types.Unary) {
switch(tokens[i][1]) {
case '-':
case '+':
tokens[i+1][1] = tokens[i+1][1]*(tokens[i][1]=='-'?-1:1);
break;
case '~':
tokens[i+1][1] = ~tokens[i+1][1];
}
tokens.splice(i, 1);
i--;
}
}
//Resolve operators
operators.forEach(op => {
for(let i = 0; i < tokens.length; i++) {
if(tokens[i][0] == types.Arithmetic && tokens[i][1] == op[0]) {
tokens[i-1][1] = op[1](tokens[i-1][1], tokens[i+1][1]);
tokens.splice(i, 2);
i-=2;
}
}
});
//Get solution
if(tokens.length == 1 && tokens[0][0] == types.Literal) {
return tokens[0][1];
}
//Something I didn't think of occured
console.error('Something went wrong solving token ' + i, tokens);
throw new Error('Something went wrong solving token ' + i);
}
}
return new Formula(string);
}