Search code examples
algorithmgraphics2dp5.jsimage-rotation

How to smoothly align rotated bitmaps side by side without jerkiness?


My current program draw a rotated bitmap (64x64) and tile it on the screen by drawing it again but adding an offset based on the computed position of the bitmap top right corner (after rotation), it works fine but i experience some jerkiness of the grid in motion.

The jerkiness doesn't appear if i do the same thing with canvas transforms.

Here is an example which compare both : https://editor.p5js.org/onirom/sketches/A5D-0nxBp

Move mouse to the left part of the canvas for the custom rotation algorithm and to the right part for the canvas one.

It seems that some tile are misplaced by a single pixel which result in the grid jerkiness.

custom rotation algorithm grid jerkiness vs canvas rotation

Is there a way to remove the grid jerkiness without doing it as a single pass and keeping the same interpolation scheme ?

Is it a sub pixels correctness issue ?

Here is some code :

let tileImage = null
function preload() {
  tileImage = loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAMeHpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjarZhpktw6DoT/8xRzBO7LcbhGzA3m+PMBlKq77eeIN0tXuKSiKBIEkImEzf7XP4/5B38hhmhiKjW3nC1/scXmOzfV3r+h385G/da/+Dzi949x83ngGQpcw/3Z/DO+GefePb/bs4l7578LvTeuc5e+HvT+jI+f4+NZ0NdfF3osCO7ubNfzwrNQ8I9Fj+nzsSi3Wn4cbc1n5/gM1a9/MRSfU3Yl8h29LSU37qu3seDPJYaGottb8+70Dry/36kem/wOLli+Q3isDPLPh864fNtQjUxkQpcQ8p2CU8dbQokJLNzuwqfbjzO/++bLR3/4+zvHsmxy9s0J8+vKv+bN5879YfxJg0/Uan4ehJ9htflz/ctxl96F3gfhs4//vnOdn51/jK/y5L19ffQt3OesevTQnKLHjC/yc6j3KHrHvCFe1Lcyn2KzIWsrN/JpfKrtdpICy052HNw354n9cdEt191xW6/TTUyMfvvC1ftpfNDBSpCan2SE00SI7vgSWlihkh5TcygG/7HF6bZNt5uu2mXsckz1jsWcJtl/+TF/d+I5giXnbP34Cru8oAErrCP8cmEaEXHncWpSB7+fX/8kroEIJnVz5YDdDnOXGMl9JVfQQAcmJq4Xe66sZwFcxNYJY1wgAja7kFzGouJ9cQ5HVgLUMd2H6AcRcCn5hZE+hpAJDuhgb94pTqf65O8wrArVAtMcCrFpoROsGBP5U2Ilh3oKKaaUciqpppZ6DlmQl3PJQs+9hBJLKrmUUk1ppddQY00111JrbbU33wL0nRo4bbW11jubdlbuvN2Z0PvwI4w40sijjDqaGX2SPjPONPMss842+/IrLAC+8iqrrrb6dptU2nGnnXfZdbfdD6l2woknnXyKOfW00z9Re8L62+c/iJp7ouY1UjKxfKLGaCnvEk7oJEnMiJiPjoAXokbESGyJma0uRi+Rk5hRj0BF8hiZJDjLScSIYNzOp+M+sXsiZ3z//8TNlKpx8/9r5IyE7m9G7ve4/VXUllSJqRG7MBSn2gD6Zuh+1MxXOwvzGokTjw8Dm0KanWNTDswM4gpmJheTS1Ss7mufpXOK7fOeBbP6STudmOyu6/TaUjpe5/lRoIY1/TC19p0zX3jIYnfPx/eG7bIA5UfWHHXGuNrQrUpvBMfXxS+mow3kPhnxWyoONxQ5c1+hTzyUWwwt75xCS+y3wPEcu/aSfDthDQg6nhb3Fhsca5h2Mu4LJ7cdSwnIJbvYeM8xZ1i9NIfDoWExhnoAD+TriMSijUqJr/yyybgkkSFAva3ZC8nIU/UfySirEylxox99yDGJfXGhrordfsQ9sq8lDGs44NwJv+2O5WmvMCsZoDbkxuHTnHFVCcCUc7s+7LoR8VRsLBPXxWKCK5OEFdcVwkqubs4/yZVWWAZvimnY0uwNvoYPJxDKuBapGJ3HAJPvC2uyFMl0mh2rndYmBCqjBcAlx7HBWA5BDKl92QrmRnjONsLOZmARyUDmT52zc+zk96q8DigOIZ5eFpSEBBXIkG1TCQ6DkClk32jUSRZa2JZmlPfibqCQm0bpjolX5+ZVlwdDUYyts3Nm+CDr6TC7ILPIkG1i68PJwcg+9s8UTPB4d5Ml0WyjVcIxMEgzZnfOzVBhVcIomdBcNKnAJ0SH9YmaHg9vj7aw5DG0phgI2YEZur+jHAij0aOhXfgkszehYf5gN8LWWsp1T/yM1fiZS25S4NhHfLRbHp5qVZECzQo+2anHCrGd55cochyi99SFPAVWFYDHvElTEPdZClqsvdYkAZezydXkUQtIJZW7Te+onrzWDfmO3ppT9FIIkSFd3HVDj6AEB7EJ+Dvl6D6pOL3GBHirxFhxn+Abe3/hCZ8RO2qwr8OmKXHdHHOA9tAN5FglTzziSTlKqAoXnYud75FhvUF8IKWdxSOS24kUshSLZYRTJNsaQSZ8TEyZgJFm1U9CqY6CsUaSofrFdM3pd6wjSiaa7Y4ASkPYs+tBdk1tSeQ+4yX1lYg1BlKMOBcJKTvClaw9BTemdnSvJEYSqXs5Bnf0bVuDYFyqB3ynOpKUIpJFiInKkDE/VeHju5m5tMWOrlIoiJ4TY+Ilrj4KQI0f7gGzrIVSvQzDnqO4O50OEta4mJYmTF9ooWT7eR2Wgiwv4cn7saDB9CGze433PMZ+HeyaEduAz+P1govTft/l+xW+lhz1Qhk+G9AcyccsbOXEgdBGmRA9/KnUcWBOPyDMTF4hJa4hER5bSZyiGHPHAFXED8QoWQMRjFSfrS77UHQ94ZSUEJAnSiMT7sYsRWQpXGDaKKgfeySfFemUD7cJ1DxLuWvDh2tF6jXKQ4NDCpPiu+H8SVmpy8wPdmBQYIJ+yNdrCFg9RpXrndJEJuUnNqgsQULvUh+ngdJG0+ylPvBhSxSMg/QexClWQYRgbMDILVMYs5AirpthD8HsDAZWcEFAvxE8i1jd75JipKEhl4SYpGbG2Z0UKWKhJeSqAWlfYY/kTR9PhWVH8e2IYhCZsyQXOE5pc3Pki3FBMKCOEkyqplSymyZkNhBSJYIlIj5sI/MknRgjo6AbzUTlD/+m3HUOdNohHaoBEsM0qZrjVqOK36nmTlwzBOIU61SdDx+OxbH0jvQYRYiEI190piH9GhpFthaoNz2T8ARKEId0qsiE3QDuw+E9Sif4KZRzvvAyf0TAe10i2ORFirWTCD25khflpNUXSEbWVUkk3Ou6/Q2nGolc9kJajMNN1iBK6ChZMaKUyRRrJJ3GSYGMqP0IlDq5UahQZdgTvsZRoJSU2saJ5EvIwOrKDLAGAgzrRXSh/fEMdy1pyXg0uvWaKujoKzbExWN6kK3YUCeNYVr3X0pGbZbsFAXi4PJdU+iFHRA+T70XPHKA6qZVXYVjpPAY2MLJlleViVbET9MnzgCqE++dFY89z0MyYFEdTvTwLScX2aziVprjW+66VrjuIayE4q/sRNDfyrU5ZoQpQs/NQwui2bus26hhFv43SpeLekCfui/aXZWKlB+lEwrSq463BHEyhGYV+lNFsUvSWmBEt+GDJFr0B545NxsNdp974AZsziL1gUZsYYgB89FiZGo021+BrzVBpCXrBodkC6Cii/lNGKvp+KN8WI5HSL350TnF5EMWXDzQQJUgloSPQifTaLWg3jabaGJhYaC6T5anoqVG8cS0ZSO9yV77I/JgDM6x0CCXZx9vgYYsWGsflr1JQENllQHV2c57LbtAX/qOvCExDiKMKV1AovlQklJRQnl0YEOUJMIs4RNp0bIhUbuYSm+gDQzJtaSaIAfFBVokxUg3dsKncLo4RvR+V1GCpBVbKEcxCMfJXlJsRhYtxVGla+Ji+4KBvC3hSsVXV4imkHwniE5hY9R2Uhl1pLpgPol4yGEEGbEeq6eRUADaDuFmUl+fUvBqAfC8lYBIDKtkLai5izJc5DvPJlWQ2IoluAF/OKodOpAYgXF3eS6q4KY3d0SNwsAalI6pFRi2BnQOHhzu4WX8RDNWOErYZfyh7TK3+ri4eMdDeAp+myTOWOWon7DF7Ff+3QpUVjqoAw6540dSmbyhzElAyKCnB/baC9KrScv11SMGaY7qbYwfugW+0rlkSrTRIqBCrDjAA0TcQm5KP+VEEQxhpyMuUv5DH+HCoG0pOYXHSX+3oze6+7feDElZaaG/DT+9liiZQnD9fGtid1csqjOlzZJUXyJ7pdtWmR0byYhjLiYe5s35uCA8L7Qk3NskmnsMdRvKH5jyRPa/7ZN4Gye+r7XntfqyueTjINsAY1kWioNybX/K0YBIohc5KFmg+YMb12yktYgjoVtyUHokLTQue0n5/OVMo95kN7SW/CdXxItZm3uvXZxiVjqtywIglCC7IL0SCb61iNOvoI8QYjDO+CHQ+hVoTtrFV8wB+JyUt4qIp4ug29SukPs0BD1Ley1SBnUJfKko3aFsb2iGhEZlGQ6KUzqiR8qSc5z6drveG4a70EZE6NNnCcsAe9GZrhZACLESRuEOUUmPkKGVIh+rSFGshzPwkVvKuqpP54LutlDzpVnVdLsfeIQf0FVfSnf6nwf1mhztvZr3JlpZUjvuIysPabOWek8A9ghClRo7ZEEOpy1xqEP8RLFRhHJ9hSc04X7uJNdnvkvyvz0zP828+FvOSWtKy2YEiWsWf8rcdIvULpG3IHhtwSR9lZ3iB6FPF7Uxul1cz1GdrZUjbKMQZmZZbaFuSNgUyNmzmrXm3yxc9DLeW7anAAABhWlDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AcxV9bpUUqDhYRcchQHaQFURFHrUIRKoRaoVUHk0u/oElDkuLiKLgWHPxYrDq4OOvq4CoIgh8gjk5Oii5S4v+SQosYD4778e7e4+4d4G9UmGp2jQOqZhnpZELI5laF4CvCCGEAMYxJzNTnRDEFz/F1Dx9f7+I8y/vcn6NXyZsM8AnEs0w3LOIN4ulNS+e8TxxhJUkhPieOGXRB4keuyy6/cS467OeZESOTnieOEAvFDpY7mJUMlXiKOKqoGuX7sy4rnLc4q5Uaa92TvzCc11aWuU5zGEksYgkiBMiooYwKLMRp1Ugxkab9hId/yPGL5JLJVQYjxwKqUCE5fvA/+N2tWZiccJPCCaD7xbY/RoDgLtCs2/b3sW03T4DAM3Cltf3VBjDzSXq9rUWPgL5t4OK6rcl7wOUOMPikS4bkSAGa/kIBeD+jb8oB/bdAz5rbW2sfpw9AhrpK3QAHh8BokbLXPd4d6uzt3zOt/n4Aru1yvz0Dqp8AAA0aaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA0LjQuMC1FeGl2MiI+CiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICB4bWxuczpHSU1QPSJodHRwOi8vd3d3LmdpbXAub3JnL3htcC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgeG1wTU06RG9jdW1lbnRJRD0iZ2ltcDpkb2NpZDpnaW1wOjNlYzk5MTljLWI0OTUtNGExMC1hNTQyLWI1NjQ4ZDc1YzcwYSIKICAgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpmMDczODg5NS1mZDBmLTRjMGUtOTRjOS0yZjA4YjZiYjVjYjEiCiAgIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoxNjkwYmU3NS02NzE1LTQwMDEtYTdkZS0xMjBjODQ3MjU3MzgiCiAgIGRjOkZvcm1hdD0iaW1hZ2UvcG5nIgogICBHSU1QOkFQST0iMi4wIgogICBHSU1QOlBsYXRmb3JtPSJMaW51eCIKICAgR0lNUDpUaW1lU3RhbXA9IjE2NzA1Mzk0OTE3Nzc3MTUiCiAgIEdJTVA6VmVyc2lvbj0iMi4xMC4zMCIKICAgdGlmZjpPcmllbnRhdGlvbj0iMSIKICAgeG1wOkNyZWF0b3JUb29sPSJHSU1QIDIuMTAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDplMGQ2NWZlOS1lMDQ1LTRhOGItYWZmZS0xZGRjMWI0M2Q3MTciCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTGludXgpIgogICAgICBzdEV2dDp3aGVuPSIyMDIyLTEyLTA4VDIzOjQ0OjUxKzAxOjAwIi8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9yeT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PnF4ZZ8AAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfmDAgWLDOSx0KrAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAABDVJREFUeNrtmzFrFVEQhSeSwiIEIhJFEUUMQVERBFNYCGKhiJWllVjbCPojLGysLS2tREghgoVFBEFEEIkERRQNYiCksNPGr3jHPW/uJi8iZqZZdt/eve/tnDlzZu59YzevXvgZHbay+iNabGpye0RE7J2eiIiIew9fDXx++/qZiIh48vzjup6vdvTQzs55rl06HhERn5bXho5n3oU3nyMiYltscRs7dmS2EwFzh/cMHfjh62pERHz5PvyN794xMXC+f9fkwHjOQRIe4lzt5eJy07ytVghwCNgsU0T0tVF5vhDw28bxCG+W2NcYdRygsZ3dxzwOCfocxl88fbDzflifLOSygOOOQoDLs8rCsDOfc3Q2e2Cq8/qd+y8GPHF+btCzeBIjX6un3f1nT+3rnJfrb9+vDOiIQoDmfY0hzcuv330bmqcx7sMun5uJiIgTM9MRETG/sNb5fOZXpTi/sNSJGMcJTk+ABDioEOA0vfO044ZMyRF7LsadlgeZ3AcSVKm2KkkMJBYCsiqv1dPu3LF1Vq05HaHX0Q2uusyuFwLIxyurkwOeUmWVxVhrDDp90LfK5HNVlhki4KLqBygH8EbUk6r4WmMZ7Q3bqgfUHj1b2lC1p+O0puB3qD6pfkBrP0Crxv/FigNcp0bZta/nWzs/60XUqBBZCKC6gt2zPK324PHiQBbR5/XVAZotYG2eXxywWRyA4tNqLEMGdT4eylZ8XJWZKUytCltjP+OKQkCm3UGEeo5xIEC1PJ7CAyhC7TtoreFMkTaqLFEI0BjkDbu1QVcjZHmeHqAa84AAagjV9HSTN6o7CgEOAcq2rabr/qzgaHfW1flZTLeyfbbi5DihEKA9N2Lb6QHNCjqeul5Z3/X2Rm0OMaUDNtoP+Fds1H2J4gB9o8qi2tPD3CqsVnPUENoL5DpZhBUfTKvKP5Vo914jOEznhbt0nkKA++DGlZOdnuNNav5Xw3Pc56pKkIRnFIHaxdWdHtzv1iKZj6OOLwQQO7wRF/PZvoGs7ueco8aqruyoQnV7fPT7qgLlHKShVGuHCAjgDbdWU+oJ2DpbhVWk4BnXaXI1hH5PN2/WX6gsAAJgZ1iY/OqMmOM+5wHtEjtEaJbRvK77F521erwQoAhQdndVmyo/p/R4HrEKZ7idpGSFTNu7DhRsrr1H17fQjlNVg1oNZtnA7eXN6m+39vi3q7/igKwabN2bM6q6Xp+70f8TuPHVEXIIoOpz9XxmsL7T6tr3Vx2QrT26bKP/GtNaQles1G7dfVoIiIgYd3W9ru+rh/AE+oCjruBo/gYBenRdaIcE17niedqHqCzQtyOkHnHr+lpvOzbXzk4rtzhE6P8PtSZRhPG9VdEWAvr+h1fvdxodJKgn3W7y7HsoAlSBuue68YWAjAP6GiytypI3rzHZ17SnqL29iOkmTlH9sOUR8AvuXvZlpzhIWwAAAABJRU5ErkJggg==')
}

function setup() {
  createCanvas(512, 512)
  
  frameRate(14)
  
  tileImage.loadPixels()
}

function computeRotatedPoint(c, s, x, y) {
  return { x: x * c - y * s, y: x * s + y * c }
}

currentTileWidth = 0
currentTileHeight = 0

// draw a rotated bitmap at screen position ox, oy
function drawRotatedBitmap(c, s, ox, oy) {
  let dcu = s
  let dcv = c
  let dru = dcv
  let drv = -dcu
  let su = (tileImage.width / 2.0) - (currentTileWidth_d2 * dcv + currentTileHeight_d2 * dcu)
  let sv = (tileImage.height / 2.0) - (currentTileWidth_d2 * drv + currentTileHeight_d2 * dru)
  let ru = su
  let rv = sv

  for (let y = 0; y < currentTileHeight; y += 1) {
    let u = ru
    let v = rv
    for (let x = 0; x < currentTileWidth; x += 1) {
      let ui = u
      let vi = v
      
      if (ui >= 0 && ui < tileImage.width) {
        let index1 = (floor(ui) + floor(vi) * tileImage.width) * 4
        let index2 = (x + ox + (y + oy) * width) * 4
        pixels[index2 + 0] = tileImage.pixels[index1 + 0]
        pixels[index2 + 1] = tileImage.pixels[index1 + 1]
        pixels[index2 + 2] = tileImage.pixels[index1 + 2]
      }
      
      u += dru
      v += drv
    }
    ru += dcu
    rv += dcv
  }
}

let angle = 0

function draw() {
  background(0)

  const s = sin(angle / 256 * PI * 2)
  const c = cos(angle / 256 * PI * 2)

  // compute rotated tile width / height
  let tw = tileImage.width
  let th = tileImage.height
  if (angle % 128 < 64) {
    currentTileWidth = abs(tw * c + th * s)
    currentTileHeight = abs(tw * s + th * c)
  } else {
    currentTileWidth = abs(tw * c - th * s)
    currentTileHeight = abs(tw * s - th * c)
  }

  currentTileWidth_d2 = (currentTileWidth / 2.0)
  currentTileHeight_d2 = (currentTileHeight / 2.0)
  
  // compute rotated point
  const rp = computeRotatedPoint(c, s, tw, 0)

  // draw tiles
  loadPixels()
  for (let i = -3; i <= 3; i += 1) {
    // compute center
    const cx = width / 2 - currentTileWidth_d2
    const cy = height / 2 - currentTileHeight_d2
    // compute tile position
    const ox = rp.x * i
    const oy = rp.y * i
    drawRotatedBitmap(c, s, round(cx + ox), round(cy + oy))
  }
  updatePixels()
  
  angle += 0.5
}
<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/addons/p5.sound.min.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8" />
  </head>
  <body>
    <main>
    </main>
  </body>
</html>


Solution

  • I have found a solution which does not use the same algorithm but use the same interpolation scheme.

    Solution with a three-shear method

    This solution use a three-pass shear method and the solution to fix the tiles jerkiness is to add the tile offset before the rotation and then round coordinates once everything is ready to be drawn :

    smooth three-shear rotation tiling

    /**
     * Bitmap rotation + stable tiling with 3-shearing method
     * The 3-shearing method is stable between -PI / 2 and PI / 2 only, that is why a flip is needed for a full rotation
     *
     * https://www.ocf.berkeley.edu/~fricke/projects/israel/paeth/rotation_by_shearing.html
     */
    let tex = null
    let tile = []
    function preload() {
      tile = loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAMeHpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjarZhpktw6DoT/8xRzBO7LcbhGzA3m+PMBlKq77eeIN0tXuKSiKBIEkImEzf7XP4/5B38hhmhiKjW3nC1/scXmOzfV3r+h385G/da/+Dzi949x83ngGQpcw/3Z/DO+GefePb/bs4l7578LvTeuc5e+HvT+jI+f4+NZ0NdfF3osCO7ubNfzwrNQ8I9Fj+nzsSi3Wn4cbc1n5/gM1a9/MRSfU3Yl8h29LSU37qu3seDPJYaGottb8+70Dry/36kem/wOLli+Q3isDPLPh864fNtQjUxkQpcQ8p2CU8dbQokJLNzuwqfbjzO/++bLR3/4+zvHsmxy9s0J8+vKv+bN5879YfxJg0/Uan4ehJ9htflz/ctxl96F3gfhs4//vnOdn51/jK/y5L19ffQt3OesevTQnKLHjC/yc6j3KHrHvCFe1Lcyn2KzIWsrN/JpfKrtdpICy052HNw354n9cdEt191xW6/TTUyMfvvC1ftpfNDBSpCan2SE00SI7vgSWlihkh5TcygG/7HF6bZNt5uu2mXsckz1jsWcJtl/+TF/d+I5giXnbP34Cru8oAErrCP8cmEaEXHncWpSB7+fX/8kroEIJnVz5YDdDnOXGMl9JVfQQAcmJq4Xe66sZwFcxNYJY1wgAja7kFzGouJ9cQ5HVgLUMd2H6AcRcCn5hZE+hpAJDuhgb94pTqf65O8wrArVAtMcCrFpoROsGBP5U2Ilh3oKKaaUciqpppZ6DlmQl3PJQs+9hBJLKrmUUk1ppddQY00111JrbbU33wL0nRo4bbW11jubdlbuvN2Z0PvwI4w40sijjDqaGX2SPjPONPMss842+/IrLAC+8iqrrrb6dptU2nGnnXfZdbfdD6l2woknnXyKOfW00z9Re8L62+c/iJp7ouY1UjKxfKLGaCnvEk7oJEnMiJiPjoAXokbESGyJma0uRi+Rk5hRj0BF8hiZJDjLScSIYNzOp+M+sXsiZ3z//8TNlKpx8/9r5IyE7m9G7ve4/VXUllSJqRG7MBSn2gD6Zuh+1MxXOwvzGokTjw8Dm0KanWNTDswM4gpmJheTS1Ss7mufpXOK7fOeBbP6STudmOyu6/TaUjpe5/lRoIY1/TC19p0zX3jIYnfPx/eG7bIA5UfWHHXGuNrQrUpvBMfXxS+mow3kPhnxWyoONxQ5c1+hTzyUWwwt75xCS+y3wPEcu/aSfDthDQg6nhb3Fhsca5h2Mu4LJ7cdSwnIJbvYeM8xZ1i9NIfDoWExhnoAD+TriMSijUqJr/yyybgkkSFAva3ZC8nIU/UfySirEylxox99yDGJfXGhrordfsQ9sq8lDGs44NwJv+2O5WmvMCsZoDbkxuHTnHFVCcCUc7s+7LoR8VRsLBPXxWKCK5OEFdcVwkqubs4/yZVWWAZvimnY0uwNvoYPJxDKuBapGJ3HAJPvC2uyFMl0mh2rndYmBCqjBcAlx7HBWA5BDKl92QrmRnjONsLOZmARyUDmT52zc+zk96q8DigOIZ5eFpSEBBXIkG1TCQ6DkClk32jUSRZa2JZmlPfibqCQm0bpjolX5+ZVlwdDUYyts3Nm+CDr6TC7ILPIkG1i68PJwcg+9s8UTPB4d5Ml0WyjVcIxMEgzZnfOzVBhVcIomdBcNKnAJ0SH9YmaHg9vj7aw5DG0phgI2YEZur+jHAij0aOhXfgkszehYf5gN8LWWsp1T/yM1fiZS25S4NhHfLRbHp5qVZECzQo+2anHCrGd55cochyi99SFPAVWFYDHvElTEPdZClqsvdYkAZezydXkUQtIJZW7Te+onrzWDfmO3ppT9FIIkSFd3HVDj6AEB7EJ+Dvl6D6pOL3GBHirxFhxn+Abe3/hCZ8RO2qwr8OmKXHdHHOA9tAN5FglTzziSTlKqAoXnYud75FhvUF8IKWdxSOS24kUshSLZYRTJNsaQSZ8TEyZgJFm1U9CqY6CsUaSofrFdM3pd6wjSiaa7Y4ASkPYs+tBdk1tSeQ+4yX1lYg1BlKMOBcJKTvClaw9BTemdnSvJEYSqXs5Bnf0bVuDYFyqB3ynOpKUIpJFiInKkDE/VeHju5m5tMWOrlIoiJ4TY+Ilrj4KQI0f7gGzrIVSvQzDnqO4O50OEta4mJYmTF9ooWT7eR2Wgiwv4cn7saDB9CGze433PMZ+HeyaEduAz+P1govTft/l+xW+lhz1Qhk+G9AcyccsbOXEgdBGmRA9/KnUcWBOPyDMTF4hJa4hER5bSZyiGHPHAFXED8QoWQMRjFSfrS77UHQ94ZSUEJAnSiMT7sYsRWQpXGDaKKgfeySfFemUD7cJ1DxLuWvDh2tF6jXKQ4NDCpPiu+H8SVmpy8wPdmBQYIJ+yNdrCFg9RpXrndJEJuUnNqgsQULvUh+ngdJG0+ylPvBhSxSMg/QexClWQYRgbMDILVMYs5AirpthD8HsDAZWcEFAvxE8i1jd75JipKEhl4SYpGbG2Z0UKWKhJeSqAWlfYY/kTR9PhWVH8e2IYhCZsyQXOE5pc3Pki3FBMKCOEkyqplSymyZkNhBSJYIlIj5sI/MknRgjo6AbzUTlD/+m3HUOdNohHaoBEsM0qZrjVqOK36nmTlwzBOIU61SdDx+OxbH0jvQYRYiEI190piH9GhpFthaoNz2T8ARKEId0qsiE3QDuw+E9Sif4KZRzvvAyf0TAe10i2ORFirWTCD25khflpNUXSEbWVUkk3Ou6/Q2nGolc9kJajMNN1iBK6ChZMaKUyRRrJJ3GSYGMqP0IlDq5UahQZdgTvsZRoJSU2saJ5EvIwOrKDLAGAgzrRXSh/fEMdy1pyXg0uvWaKujoKzbExWN6kK3YUCeNYVr3X0pGbZbsFAXi4PJdU+iFHRA+T70XPHKA6qZVXYVjpPAY2MLJlleViVbET9MnzgCqE++dFY89z0MyYFEdTvTwLScX2aziVprjW+66VrjuIayE4q/sRNDfyrU5ZoQpQs/NQwui2bus26hhFv43SpeLekCfui/aXZWKlB+lEwrSq463BHEyhGYV+lNFsUvSWmBEt+GDJFr0B545NxsNdp974AZsziL1gUZsYYgB89FiZGo021+BrzVBpCXrBodkC6Cii/lNGKvp+KN8WI5HSL350TnF5EMWXDzQQJUgloSPQifTaLWg3jabaGJhYaC6T5anoqVG8cS0ZSO9yV77I/JgDM6x0CCXZx9vgYYsWGsflr1JQENllQHV2c57LbtAX/qOvCExDiKMKV1AovlQklJRQnl0YEOUJMIs4RNp0bIhUbuYSm+gDQzJtaSaIAfFBVokxUg3dsKncLo4RvR+V1GCpBVbKEcxCMfJXlJsRhYtxVGla+Ji+4KBvC3hSsVXV4imkHwniE5hY9R2Uhl1pLpgPol4yGEEGbEeq6eRUADaDuFmUl+fUvBqAfC8lYBIDKtkLai5izJc5DvPJlWQ2IoluAF/OKodOpAYgXF3eS6q4KY3d0SNwsAalI6pFRi2BnQOHhzu4WX8RDNWOErYZfyh7TK3+ri4eMdDeAp+myTOWOWon7DF7Ff+3QpUVjqoAw6540dSmbyhzElAyKCnB/baC9KrScv11SMGaY7qbYwfugW+0rlkSrTRIqBCrDjAA0TcQm5KP+VEEQxhpyMuUv5DH+HCoG0pOYXHSX+3oze6+7feDElZaaG/DT+9liiZQnD9fGtid1csqjOlzZJUXyJ7pdtWmR0byYhjLiYe5s35uCA8L7Qk3NskmnsMdRvKH5jyRPa/7ZN4Gye+r7XntfqyueTjINsAY1kWioNybX/K0YBIohc5KFmg+YMb12yktYgjoVtyUHokLTQue0n5/OVMo95kN7SW/CdXxItZm3uvXZxiVjqtywIglCC7IL0SCb61iNOvoI8QYjDO+CHQ+hVoTtrFV8wB+JyUt4qIp4ug29SukPs0BD1Ley1SBnUJfKko3aFsb2iGhEZlGQ6KUzqiR8qSc5z6drveG4a70EZE6NNnCcsAe9GZrhZACLESRuEOUUmPkKGVIh+rSFGshzPwkVvKuqpP54LutlDzpVnVdLsfeIQf0FVfSnf6nwf1mhztvZr3JlpZUjvuIysPabOWek8A9ghClRo7ZEEOpy1xqEP8RLFRhHJ9hSc04X7uJNdnvkvyvz0zP828+FvOSWtKy2YEiWsWf8rcdIvULpG3IHhtwSR9lZ3iB6FPF7Uxul1cz1GdrZUjbKMQZmZZbaFuSNgUyNmzmrXm3yxc9DLeW7anAAABhWlDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AcxV9bpUUqDhYRcchQHaQFURFHrUIRKoRaoVUHk0u/oElDkuLiKLgWHPxYrDq4OOvq4CoIgh8gjk5Oii5S4v+SQosYD4778e7e4+4d4G9UmGp2jQOqZhnpZELI5laF4CvCCGEAMYxJzNTnRDEFz/F1Dx9f7+I8y/vcn6NXyZsM8AnEs0w3LOIN4ulNS+e8TxxhJUkhPieOGXRB4keuyy6/cS467OeZESOTnieOEAvFDpY7mJUMlXiKOKqoGuX7sy4rnLc4q5Uaa92TvzCc11aWuU5zGEksYgkiBMiooYwKLMRp1Ugxkab9hId/yPGL5JLJVQYjxwKqUCE5fvA/+N2tWZiccJPCCaD7xbY/RoDgLtCs2/b3sW03T4DAM3Cltf3VBjDzSXq9rUWPgL5t4OK6rcl7wOUOMPikS4bkSAGa/kIBeD+jb8oB/bdAz5rbW2sfpw9AhrpK3QAHh8BokbLXPd4d6uzt3zOt/n4Aru1yvz0Dqp8AAA0aaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA0LjQuMC1FeGl2MiI+CiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICB4bWxuczpHSU1QPSJodHRwOi8vd3d3LmdpbXAub3JnL3htcC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgeG1wTU06RG9jdW1lbnRJRD0iZ2ltcDpkb2NpZDpnaW1wOjNlYzk5MTljLWI0OTUtNGExMC1hNTQyLWI1NjQ4ZDc1YzcwYSIKICAgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpmMDczODg5NS1mZDBmLTRjMGUtOTRjOS0yZjA4YjZiYjVjYjEiCiAgIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoxNjkwYmU3NS02NzE1LTQwMDEtYTdkZS0xMjBjODQ3MjU3MzgiCiAgIGRjOkZvcm1hdD0iaW1hZ2UvcG5nIgogICBHSU1QOkFQST0iMi4wIgogICBHSU1QOlBsYXRmb3JtPSJMaW51eCIKICAgR0lNUDpUaW1lU3RhbXA9IjE2NzA1Mzk0OTE3Nzc3MTUiCiAgIEdJTVA6VmVyc2lvbj0iMi4xMC4zMCIKICAgdGlmZjpPcmllbnRhdGlvbj0iMSIKICAgeG1wOkNyZWF0b3JUb29sPSJHSU1QIDIuMTAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDplMGQ2NWZlOS1lMDQ1LTRhOGItYWZmZS0xZGRjMWI0M2Q3MTciCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTGludXgpIgogICAgICBzdEV2dDp3aGVuPSIyMDIyLTEyLTA4VDIzOjQ0OjUxKzAxOjAwIi8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9yeT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PnF4ZZ8AAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfmDAgWLDOSx0KrAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAABDVJREFUeNrtmzFrFVEQhSeSwiIEIhJFEUUMQVERBFNYCGKhiJWllVjbCPojLGysLS2tREghgoVFBEFEEIkERRQNYiCksNPGr3jHPW/uJi8iZqZZdt/eve/tnDlzZu59YzevXvgZHbay+iNabGpye0RE7J2eiIiIew9fDXx++/qZiIh48vzjup6vdvTQzs55rl06HhERn5bXho5n3oU3nyMiYltscRs7dmS2EwFzh/cMHfjh62pERHz5PvyN794xMXC+f9fkwHjOQRIe4lzt5eJy07ytVghwCNgsU0T0tVF5vhDw28bxCG+W2NcYdRygsZ3dxzwOCfocxl88fbDzflifLOSygOOOQoDLs8rCsDOfc3Q2e2Cq8/qd+y8GPHF+btCzeBIjX6un3f1nT+3rnJfrb9+vDOiIQoDmfY0hzcuv330bmqcx7sMun5uJiIgTM9MRETG/sNb5fOZXpTi/sNSJGMcJTk+ABDioEOA0vfO044ZMyRF7LsadlgeZ3AcSVKm2KkkMJBYCsiqv1dPu3LF1Vq05HaHX0Q2uusyuFwLIxyurkwOeUmWVxVhrDDp90LfK5HNVlhki4KLqBygH8EbUk6r4WmMZ7Q3bqgfUHj1b2lC1p+O0puB3qD6pfkBrP0Crxv/FigNcp0bZta/nWzs/60XUqBBZCKC6gt2zPK324PHiQBbR5/XVAZotYG2eXxywWRyA4tNqLEMGdT4eylZ8XJWZKUytCltjP+OKQkCm3UGEeo5xIEC1PJ7CAyhC7TtoreFMkTaqLFEI0BjkDbu1QVcjZHmeHqAa84AAagjV9HSTN6o7CgEOAcq2rabr/qzgaHfW1flZTLeyfbbi5DihEKA9N2Lb6QHNCjqeul5Z3/X2Rm0OMaUDNtoP+Fds1H2J4gB9o8qi2tPD3CqsVnPUENoL5DpZhBUfTKvKP5Vo914jOEznhbt0nkKA++DGlZOdnuNNav5Xw3Pc56pKkIRnFIHaxdWdHtzv1iKZj6OOLwQQO7wRF/PZvoGs7ueco8aqruyoQnV7fPT7qgLlHKShVGuHCAjgDbdWU+oJ2DpbhVWk4BnXaXI1hH5PN2/WX6gsAAJgZ1iY/OqMmOM+5wHtEjtEaJbRvK77F521erwQoAhQdndVmyo/p/R4HrEKZ7idpGSFTNu7DhRsrr1H17fQjlNVg1oNZtnA7eXN6m+39vi3q7/igKwabN2bM6q6Xp+70f8TuPHVEXIIoOpz9XxmsL7T6tr3Vx2QrT26bKP/GtNaQles1G7dfVoIiIgYd3W9ru+rh/AE+oCjruBo/gYBenRdaIcE17niedqHqCzQtyOkHnHr+lpvOzbXzk4rtzhE6P8PtSZRhPG9VdEWAvr+h1fvdxodJKgn3W7y7HsoAlSBuue68YWAjAP6GiytypI3rzHZ17SnqL29iOkmTlH9sOUR8AvuXvZlpzhIWwAAAABJRU5ErkJggg==')
    
    }
    
    function setup() {
      createCanvas(512, 512)
      
      frameRate(12)
    }
    
    function computeRotatedPoint(c, s, x, y) {
      return { x: x * c - y * s, y: x * s + y * c }
    }
    
    function _shearX(t, x, y) {
      return x - y * t
    }
    
    function _shearY(s, x, y) {
      return x * s + y
    }
    
    currentTileWidth = 0
    currentTileHeight = 0
    
    currentTileLookupFunction = tileLookup1
    
    // regular lookup
    function tileLookup1(x, y) {
      return (x + y * tile.width) * 4
    }
    
    // flipped lookup
    function tileLookup2(x, y) {
      return ((tile.width - 1 - x) + (tile.height - 1 - y) * tile.width) * 4
    }
    
    // draw a rotated bitmap at offset ox,oy with cx,cy as center of rotation offset
    function drawRotatedBitmap(c, s, t, ox, oy, cx, cy) {
      for (let ty = 0; ty < tile.height; ty += 1) {
        for (let tx = 0; tx < tile.width; tx += 1) {
          // center of rotation
          let scx = tile.width - tx - tile.width / 2
          let scy = tile.height - ty - tile.height / 2
          
          // this is key to a stable rotation without any jerkiness
          scx += cx
          scy += cy
          
          // shear
          let ux = round(_shearX(t, scx, scy))
          let uy = round(_shearY(s, ux, scy))
          ux = round(_shearX(t, ux, uy))
          
          // translate again
          ux = currentTileWidth_d2 - ux
          uy = currentTileHeight_d2 - uy
    
          // plot with offset
          let index1 = currentTileLookupFunction(tx, ty)
          let index2 = (ox + ux + (oy + uy) * width) * 4
    
          pixels[index2 + 0] = tile.pixels[index1 + 0]
          pixels[index2 + 1] = tile.pixels[index1 + 1]
          pixels[index2 + 2] = tile.pixels[index1 + 2]
        }
      }
    }
    
    let angle = -3.141592653 / 2
    function draw() {
      const s = sin(angle)
      const c = cos(angle)
      const t = tan(angle / 2)
    
      tile.loadPixels()
    
      background(0)
    
      // compute rotated tile width / height
      let tw = tile.width
      let th = tile.height
    
      currentTileWidth = abs(tw * c + th * s)
      currentTileHeight = abs(tw * s + th * c)
    
      currentTileWidth_d2 = round(currentTileWidth / 2.0)
      currentTileHeight_d2 = round(currentTileHeight / 2.0)
    
      // draw tiles
      loadPixels()
      for (let j = -2; j <= 2; j += 1) {
        for (let i = -2; i <= 2; i += 1) {
          let ox = round(width / 2 - currentTileWidth_d2)
          let oy = round(height / 2 - currentTileHeight_d2)
          drawRotatedBitmap(c, s, t, ox, oy, i * tw, j * tw)
        }
      }
      updatePixels()
      
      angle += 0.025
      if (angle >= PI / 2) {
        angle -= PI
        
        if (currentTileLookupFunction === tileLookup2) {
          currentTileLookupFunction = tileLookup1
        } else {
          currentTileLookupFunction = tileLookup2
        }
      }
    }
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/addons/p5.sound.min.js"></script>
        <link rel="stylesheet" type="text/css" href="style.css">
        <meta charset="utf-8" />
      </head>
      <body>
        <main>
        </main>
      </body>
    </html>


    I cannot say technically why it works but it is probably related to an error accumulation issue / rounding since i can reproduce the question issue completely with the three-shear method if i add the tile offset after rotation and round the offset and shear pass independently such as :

    function drawRotatedBitmap(c, s, t, ox, oy, cx, cy) {
      cx = round(_shearX(t, cx, cy))
      cy = round(_shearY(s, cx, cy))
      cx = round(_shearX(t, cx, cy))
      for (let ty = 0; ty < tex.height; ty += 1) {
        ...
        let ux = round(_shearX(t, scx, scy))
        let uy = round(_shearY(s, ux, scy))
        ux = round(_shearX(t, ux, uy))
        ...
        let index2 = (cx + ux + ox + (cy + uy + oy) * width) * 4
        ...
      }
    }
    

    three-shear rotation tiling with tiles jerkiness


    The issue become clearly visible if i round the offset and the shearing result at the same time which result in missing pixels in the final image such as :

    function drawRotatedBitmap(c, s, t, ox, oy, cx, cy) {
      cx = _shearX(t, cx, cy)
      cy = _shearY(s, cx, cy)
      cx = _shearX(t, cx, cy)
      for (let ty = 0; ty < tex.height; ty += 1) {
        ...
        let ux = _shearX(t, scx, scy)
        let uy = _shearY(s, ux, scy)
        ux = _shearX(t, ux, uy)
        ...
        let index2 = (round(cx + ux) + ox + (round(cy + uy) + oy) * width) * 4
        ...
      }
    }
    

    three-shear rotation tiling with holes


    I would still like a detailed explanation of the jerkiness behavior and to know if there is a smooth solution by adding the tile offset after the rotation, it seems that the jiggling is due to the center of rotation being off one or two pixels depending on the angle.