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