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.
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,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W2sfpw9AhrpK3QAHh8BokbLXPd4d6uzt3zOt/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>
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 :
/**
* 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
...
}
}
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
...
}
}
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.
This is definitely a pixelization problem. Analytically (vectorial) one can't explain the jiggling. It can be minimized, e.g. by rotating around the center of the whole image the successive pixels of a line and so forth line-by-line of the whole image, but the jiggling cannot be cancelled. Ultimately, this corresponds to creating an image object and rotating it around its center!
Is there a way to remove the grid jerkiness without doing it as a single pass and keeping the same interpolation scheme ?
No
Is it a sub pixels correctness issue ?
No, it is an interpolation issue
The frameCount is a function of the frameRate (which is not a constant contrary to what we think even if we set it), the execution time of the cycling draw function and the runtime environment (canvas or the gif one). It seems that for the gif the frameCount is reset to zero after a certain cumulated count, which corresponds to a reset to the vertical position of the image.
I tried to reproduce the "jerking effect" by changing the 64 parameter in the following instruction and the frameRate, without success.
if (frameCount % 128 < 64) {
I suggest to make the rotation speed independent of the frameCount.
FWIW the WEBGL canvas will be a bit faster in getting a result your code already hints at using image() and because you're using power of 2 dimensions you could also make use of textureWrap():
// this variable will hold our shader object
let bricks;
function preload() {
bricks = loadImage(
"data:image/png;base64,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W2sfpw9AhrpK3QAHh8BokbLXPd4d6uzt3zOt/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() {
// use WEBGL renderer,
createCanvas(512, 512, WEBGL);
// if helps the sketch dimensions are a power of 2 so textureWrap() can be used
textureWrap(REPEAT);
noStroke();
}
function draw() {
background(0);
// full 7 block width and height
const w = 64 * 7;
const h = 64;
// half the dimensions to rotate from center
const hw = int(w / 2);
const hh = int(h / 2);
rotate(frameCount * 0.03);
texture(bricks);
// vertex( x, y, u, v ) (by default u,v are in pixel coordinates)
// otherwise use textMode(NORMAL); in setup()
beginShape();
vertex(-hw, -hh, -hw, -hh); //TL
vertex(+hw, -hh, +hw, -hh); //TR
vertex(+hw, +hh, +hw, +hh); //BR
vertex(-hw, +hh, -hw, +hh); //BL
endShape();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.min.js"></script>
An overcomplicated version of the above is to use a shader for rotation which can produce interesting results:
It seems you're more interested in working out a custom rotation algorithms that produces less artefacts.
If so, you might also want to look at a RotSprite.
Doing a quick search I see implementations such as this shader one or this js one.
Got a p5.js sketch that I want to update when I change the body color so I can use the animation in dark mode as well. However, when I implement the interaction for the body color changes the graphic fails to update its color. Not sure if I need to create a new function and then call it in Draw again?
//p5.js hero animation//
let font
let graphic
let body = document.querySelector("body")
let bodyTextColor = window.getComputedStyle(body).getPropertyValue("color");
function responsive () {
graphic = createGraphics(windowWidth / 1.1, windowHeight)
graphic.fill(bodyTextColor);
graphic.textSize(windowWidth * 0.4);
graphic.textAlign(CENTER, CENTER);
graphic.textFont('GT cinetype');
graphic.textStyle(ITALIC);
graphic.text("aaaa", windowWidth / 2, windowHeight / 2);
}
function setup () {
var myCanvas = createCanvas(windowWidth, windowHeight);
myCanvas.parent('p5-container');
responsive();
}
function draw () {
background("0, 0, 0, 255");
const tileSize = 20
for (let x = 0; x < 120; x = x + 1) {
for (let y = 0; y < 60; y = y + 1) {
const wave = 0.03
const distortionX = sin(frameCount * wave + x * 0.5 + y * 0.2) * 30
const distortionY = sin(frameCount * wave + x * 0.5 + y * 1.1) * 10
const sx = x * tileSize + distortionX
const sy = y * tileSize + distortionY
const sw = tileSize
const sh = tileSize
const dx = x * tileSize
const dy = y * tileSize
const dw = tileSize
const dh = tileSize
image(graphic, dx, dy, dw, dh, sx, sy, sw, sh)
graphic.fill(bodyTextColor);
}
}
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
responsive();
}
here is the link to the temporary site https://jaramillo-arango.webflow.io/
I'm using D3.js to create a globe. I have a working SVG wife-frame version, and I'm also trying to create a more detailed textured one, a two-mode thing.
The image I'm using from an API is square:
Which doesn't really work out well when projected to orthographic, it's a lot more "squished" towards the equator than it should be:
Not doing anything particularly special:
const dx = 2048;
const dy = 2048;
const width = 2048;
const height = 2048;
let sourceData = mapImage.getImageData(0, 0, dx, dy).data,
target = ctx.createImageData(width, height),
targetData = target.data;
for (let y = 0, i = -1; y < height; ++y) {
for (let x = 0; x < width; ++x) {
let p = projection.invert([x, y]);
if (p[0] > 180 || p[0] < -180 || p[1] > 90 || p[1] < -90) {
i += 4;
continue;
}
let q = ((90 - p[1]) / 180 * dy | 0) * dx + ((180 + p[0]) / 360 * dx | 0) << 2;
targetData[++i] = sourceData[q];
targetData[++i] = sourceData[++q];
targetData[++i] = sourceData[++q];
targetData[++i] = 255;
}
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(target, 0, 0);
I'm wondering if there's a straightforward way to make the additional adjustment for the stretching of the map image?
(Bonus points if you can also point me to why the space around the globe is not transparent? But that's not the main question here.)
I'm generating image programmatically inside canvas.
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
// here I have some code in loop setting individual pixels
// ...
//
// save image to variable
var dataURL = canvas.toDataURL();
How can I rotate created image by 90 degrees?
EDIT:
This is not duplicate because I don't draw image, it is never visible. I only want to generate it, rotate it and save to variable.
EDIT2:
I'm trying to rotate it with this code:
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate(90 * Math.PI / 180)
But it doesn't work
EDIT3:
This is more complex example of my code:
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
canvas.setPixel = function (x, y, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, 1, 1);
}
for (var i in data) {
for (var j in data[i]) {
switch (data[i][j]) {
case 1:
var color = '#ffff00',
type = 'w'
break
case 3:
var rgb = (256 - parseInt(pixels[i][j]) - minus.grass).toString(16),
color = '#00' + rgb + '00',
type = 'g'
break
case 4:
var rgb = (256 - parseInt(pixels[i][j]) - minus.hills).toString(16),
color = '#' + rgb + rgb + '00',
type = 'h'
break
case 5:
var rgb = (parseInt(pixels[i][j]) + minus.mountains).toString(16),
color = '#' + rgb + rgb + rgb,
type = 'm'
break
case 6:
var rgb = (parseInt(pixels[i][j]) + minus.snow).toString(16),
color = '#' + rgb + rgb + rgb,
type = 'm'
break
}
if (i % fieldSize == 0 && j % fieldSize == 0) {
if (notSet(fields[y])) {
fields[y] = []
}
fields[y][x] = type
x++
}
canvas.setPixel(i, j, color)
}
if (i % fieldSize == 0) {
x = 0
y++
}
}
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate(90 * Math.PI / 180)
var token = {
type: 'save',
map: canvas.toDataURL('image/png')
}
ws.send(JSON.stringify(token))
To rotate image by 90 degrees I had to put
ctx.translate(0, canvas.height)
ctx.rotate(270 * Math.PI / 180)
before
for (var i in data) {
for (var j in data[i]) {
switch (data[i][j]) {
// ... drawing pixels
}
}
}
I designed a web app with html5 canvas. To export an image, the code will be below:
var img = canvas.toDataURL("image/png");
Is there any way to export a 2x image?
It is for hdpi display like apple retina display.
Yes there are a few ways but every time you stretch a non vector image you will get some pixel distortion. However if its only two times the size you could get away with it using nearest neighbor. The below example shows two different methods, one is just stretching the image, the other uses nearest neighbor with a zoom factor of two.
Live Demo
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
canvas2 = document.getElementById("canvas2"),
ctx2 = canvas2.getContext("2d"),
tempCtx = document.createElement('canvas').getContext('2d'),
img = document.getElementById("testimg"),
zoom = 2;
tempCtx.drawImage(img, 0, 0);
var imgData = tempCtx.getImageData(0, 0, img.width, img.height).data;
canvas.width = img.width * zoom;
canvas.height = img.height * zoom;;
// nearest neighbor
for (var x = 0; x < img.width; ++x) {
for (var y = 0; y < img.height; ++y) {
var i = (y * img.width + x) * 4;
var r = imgData[i];
var g = imgData[i + 1];
var b = imgData[i + 2];
var a = imgData[i + 3];
ctx.fillStyle = "rgba(" + r + "," + g + "," + b + "," + (a / 255) + ")";
ctx.fillRect(x * zoom, y * zoom, zoom, zoom);
}
}
// stretched
ctx2.drawImage(img, 0, 0, 140, 140);
#phrogz has a great example of this here as well, showing a few different ways you can accomplish image re-sizing.