Yet more spirographs ➿

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);
}