标签 javascript graphics html5-canvas textures scaling
我正在使用 HTML5 Canvas API 为像素艺术游戏绘制瓷砖 map 。渲染的瓦片 map 由许多较小的图像组成,这些图像是从称为瓦片表的单个源图像中剪切出来的。我正在使用 drawImage(src_img, sx, sy, sw, sh, dx, dy, dw, dh)从源图像中切出单个图块并将它们绘制到目标 Canvas 上。我正在使用 setTransform(sx, 0, 0, sy, tx, ty)将缩放和平移应用于最终渲染的图像。我需要修复的颜色“出血”问题是由采样器引起的,它在缩放操作期间使用插值来混合颜色,以使事物看起来不会像素化。这对于缩放数码照片非常有用,但不适用于像素艺术。虽然这不会对图块的中心造成太大的视觉损害,但采样器会沿着源图像中相邻图块的边缘混合颜色,从而在渲染的图块 map 中产生意想不到的颜色。而不是仅使用传递给 drawImage 的源矩形内的颜色,采样器从其边界之外混合颜色,导致瓷砖之间出现间隙。下面是我的瓷砖表的源图像。它的实际大小是 24x24 像素,但我在 GIMP 中将其放大到 96x96 像素,以便您可以看到它。我在 GIMP 的缩放工具上使用了“插值:无”设置。正如您所看到的,由于采样器没有对颜色进行插值,因此各个图块周围没有间隙或模糊的边界。即使在 imageSmoothingEnabled 时, Canvas API 的采样器显然也会插入颜色。设置为 false . 下面是带有 imageSmoothingEnabled 的渲染图块 map 的一部分设置为 true .左箭头指向灰色瓷砖底部的一些红色出血。这是因为红色瓷砖在瓷砖表中的灰色瓷砖正下方。采样器正在将红色混合到灰色瓷砖的底部边缘。右侧的箭头指向绿色图块的右边缘。如您所见,没有颜色渗入其中。这是因为源图像中绿色图块的右侧没有任何东西,因此采样器没有任何东西可以混合。 下面是带有 imageSmoothingEnabled 的渲染图块 map 设置为 false .根据比例和翻译,仍然会发生纹理渗色。左箭头指向从源图像中的红色瓷砖渗出的红色。视觉伤害减少了,但仍然存在。右箭头指向最右侧绿色磁贴的问题,它有一条细灰色线从源图像中的灰色磁贴渗入,位于绿色磁贴的左侧。 上面的两张图片是从 Edge 截屏的。 Chrome 和 Firefox 在隐藏出血方面做得更好。 Edge 似乎在四面八方流血,但 Chrome 和 Firefox 似乎只在源矩形的右侧和底部流血。如果有人知道如何解决这个问题,请告诉我。人们在很多论坛上询问这个问题,并得到解决以下问题的答案:
drawImage(src_img, sx, sy, sw, sh, dx, dy, dw, dh)
setTransform(sx, 0, 0, sy, tx, ty)
drawImage
imageSmoothingEnabled
false
true
image-rendering:pixelated
function drawRounded(source_image, context, scale) { var offset_x = -OFFSET.x * scale + context.canvas.width * 0.5; var offset_y = -OFFSET.y * scale + context.canvas.height * 0.5; var map_height = (MAP_HEIGHT * scale)|0; // Similar to Math.trunc(MAP_HEIGHT * scale); var map_width = (MAP_WIDTH * scale)|0; var tile_size = TILE_SIZE * scale; var rendered_tile_size = (tile_size + 1)|0; // Similar to Math.ceil(tile_size); var map_index = 0; // Track the tile index in the map. This increases once per draw loop. /* Loop through all tile positions in actual coordinate space so no additional calculations based on grid index are needed. */ for (var y = 0; y < map_height; y += tile_size) { // y first so we draw rows from top to bottom for (var x = 0; x < map_width; x += tile_size) { var frame = FRAMES[MAP[map_index]]; // The frame is the source location of the tile in the source_image. // We have to keep the dx, dy truncation inside the loop to ensure the highest level of accuracy possible. context.drawImage(source_image, frame.x, frame.y, TILE_SIZE, TILE_SIZE, (offset_x + x)|0, (offset_y + y)|0, rendered_tile_size, rendered_tile_size); map_index ++; } } }
最佳答案
这是一个四舍五入的问题。已经有 that question当上下文准确地转换为 Safari 浏览器时遇到的这个问题 n.5 , Edge 和 IE 更糟,总是以一种或另一种方式流血,MacOs 版 Chrome 在 n.5 上流血也是,但只有在绘制 时, 才可以。至少可以说,这是一个有问题的区域。我没有检查规范以确切知道他们应该做什么,但有一个简单的解决方法。自己计算坐标的变换,以便您可以准确控制它们将如何四舍五入。这样您甚至不需要关闭图像平滑算法,您将始终拥有清晰的像素,因为您将始终在像素边界上进行绘制:// First calculate the scaled translations const scaled_offset_left = -OFFSET.x * scale + context.canvas.width * 0.5; const scaled_offset_top = -OFFSET.y * scale + context.canvas.height * 0.5; // when drawing each tile const dest_x = Math.floor( scaled_offset_left + (x * scale) ); const dest_y = Math.floor( scaled_offset_top + (y * scale) ); const dest_size = Math.ceil( TILE_SIZE * scale ); context.drawImage( source_image, frame.x, frame.y, TILE_SIZE, TILE_SIZE, dest_x, dest_y, dest_size, dest_size, ); /* This is the tile map. Each value is a frame index in the FRAMES array. Each frame tells drawImage where to blit the source from */ const MAP = [ 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 0, 1, 0, 1, 2, 2, 1, 2, 3, 2, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 3, 4, 3, 4, 5, 5, 4, 5, 6, 5, 3, 4, 3, 4, 5, 5, 4, 5, 6, 5, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 6, 7, 6, 7, 8, 8, 7, 8, 0, 8, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8 ]; const TILE_SIZE = 8; // Each tile is 8x8 pixels const MAP_HEIGHT = 80; // The map is 80 pixels tall by 80 pixels wide const MAP_WIDTH = 80; /* Each frame represents the source x, y coordinates of a tile in the source image. They are indexed according to the map values */ const FRAMES = [ { x:0, y:0 }, // map value = 0 { x:8, y:0 }, // map value = 1 { x:16, y:0 }, // map value = 2 { x:0, y:8 }, // etc. { x:8, y:8 }, { x:16, y:8}, { x:0, y:16}, { x:8, y:16}, { x:16, y:16} ]; /* These represent the state of the keyboard keys being used. false is up and true is down */ const KEYS = { down: false, left: false, right: false, scale_down: false, // the D key scale_up: false, // the F key up: false } /* This is the scroll offset. You can also think of it as the position of the red dot in the map. */ const OFFSET = { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.5 }; // It starts out centered in the map. const MAX_SCALE = 75; // Max scale is 75 times larger than the actual image size. const MIN_SCALE = 0; // Texture bleeding seems to only occur on upscale, but min scale is 0 in case you want to try it. var scale = 4.71; // some arbitrary number that will hopefully cause the issue in your browser /* Get the canvas drawing context. */ var context = document.querySelector('canvas').getContext('2d', { alpha: false, desynchronized: true }); /* The toggle button is the div */ var toggle = document.querySelector('div'); /* The source image is a 24x24 square with 9 tile images of various colors in it. */ var base_64_image_source = 'data:image/png;base64,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'; var source_image = new Image(); // This will be the source image /* The keyboard event handler */ function keyDownUp(event) { var state = event.type == 'keydown' ? true : false; switch (event.keyCode) { case 37: KEYS.left = state; break; case 38: KEYS.up = state; break; case 39: KEYS.right = state; break; case 40: KEYS.down = state; break; case 68: KEYS.scale_down = state; break; case 70: KEYS.scale_up = state; } } /* This is the update and rendering loop. It handles input and draws the images. */ function loop() { window.requestAnimationFrame(loop); // Perpetuate the loop /* Prepare to move and scale the image with the keyboard input */ if (KEYS.left) OFFSET.x -= 0.5; if (KEYS.right) OFFSET.x += 0.5; if (KEYS.up) OFFSET.y -= 0.5; if (KEYS.down) OFFSET.y += 0.5; if (KEYS.scale_down) scale -= 0.5 * scale / MAX_SCALE; if (KEYS.scale_up) scale += 0.5 * scale / MAX_SCALE; /* Keep the scale size within a defined range */ if (scale > MAX_SCALE) scale = MAX_SCALE; else if (scale < MIN_SCALE) scale = MIN_SCALE; /* Clear the canvas to gray. */ context.setTransform(1, 0, 0, 1, 0, 0); // Set the transform back to the identity matrix context.fillStyle = "#202830"; // Set the fill color to gray context.fillRect(0, 0, context.canvas.width, context.canvas.height); // fill the entire canvas /* [EDIT] Don't set the transform, we will calculate it ourselves // context.setTransform(scale, 0, 0, scale, -OFFSET.x * scale + context.canvas.width * 0.5, -OFFSET.y * scale + context.canvas.height * 0.5); First step is calculating the scaled translation */ const scaled_offset_left = -OFFSET.x * scale + context.canvas.width * 0.5; const scaled_offset_top = -OFFSET.y * scale + context.canvas.height * 0.5; let map_index = 0; // Track the tile index in the map. This increases once per draw loop. /* Loop through all tile positions in actual coordinate space so no additional calculations based on grid index are needed. */ for (let y = 0; y < MAP_HEIGHT; y += TILE_SIZE) { // y first so we draw rows from top to bottom for (let x = 0; x < MAP_WIDTH; x += TILE_SIZE) { const frame = FRAMES[MAP[map_index]]; // The frame is the source location of the tile in the source_image. /* [EDIT] We transform the coordinates ourselves We can control a uniform rounding by using floor and ceil */ const dest_x = Math.floor( scaled_offset_left + (x * scale) ); const dest_y = Math.floor( scaled_offset_top + (y * scale) ); const dest_size = Math.ceil(TILE_SIZE * scale); context.drawImage( source_image, frame.x, frame.y, TILE_SIZE, TILE_SIZE, dest_x, dest_y, dest_size, dest_size ); map_index++; } } /* Draw the red dot in the center of the screen. */ context.fillStyle = "#ff0000"; /* [EDIT] Do the same kind of calculations for the "dot" if you don't want antialiasing // const dot_x = Math.floor( scaled_offset_left + ((OFFSET.x - 0.5) * scale) ); // const dot_y = Math.floor( scaled_offset_top + ((OFFSET.y - 0.5) * scale) ); // const dot_size = Math.ceil( scale ); // context.fillRect( dot_x, dot_y, dot_size, dot_size ); // center on the dot But if you do want antialiasing for the dot, then just set the transformation for this drawing */ context.setTransform(scale, 0, 0, scale, scaled_offset_left, scaled_offset_top); context.fillRect( (OFFSET.x - 0.5), (OFFSET.y - 0.5), 1, 1 ); // center on the dot var smoothing = context.imageSmoothingEnabled; // Get the current smoothing value because we are going to ignore it briefly. /* Draw the source image in the top left corner for reference. */ context.setTransform(4, 0, 0, 4, 0, 0); // Zoom in on it so it's visible. context.imageSmoothingEnabled = false; // Set smoothing to false so we get a crisp source image representation (the real source image is not scaled at all). context.drawImage( source_image, 0, 0 ); context.imageSmoothingEnabled = smoothing; // Set smoothing back the way it was according to the toggle choice. } /* Turn image smoothing on and off when you press the toggle. */ function toggleSmoothing(event) { context.imageSmoothingEnabled = !context.imageSmoothingEnabled; if (context.imageSmoothingEnabled) toggle.innerText = 'Smoothing Enabled'; // Make sure the button has appropriate text in it. else toggle.innerText = 'Smoothing Disabled'; } /* The main loop will start after the source image is loaded to ensure there is something to draw. */ source_image.addEventListener('load', (event) => { window.requestAnimationFrame(loop); // Start the loop }, { once: true }); /* Add the toggle smoothing click handler to the div. */ toggle.addEventListener('click', toggleSmoothing); /* Add keyboard input */ window.addEventListener('keydown', keyDownUp); window.addEventListener('keyup', keyDownUp); /* Resize the canvas. */ context.canvas.width = 480; context.canvas.height = 480; toggleSmoothing(); // Set imageSmoothingEnabled /* Load the source image from the base64 string. */ source_image.setAttribute('src', base_64_image_source); * { box-sizing: border-box; margin: 0; overflow: hidden; padding: 0; user-select: none; } body, html { background-color: #202830; color: #ffffff; height: 100%; width: 100%; } body { align-items: center; display: grid; justify-items: center; } p { max-width: 640px; } div { border: #ffffff 2px solid; bottom: 4px; cursor: pointer; padding: 8px; position: fixed; right: 4px } <div>Smoothing Disabled</div> <p>Use the arrow keys to scroll and the D and F keys to scale. The source image is represented on the top left. Notice the vertical and horizontal lines that appear between tiles as you scroll and scale. They are the color of the tile's neighbor in the source image. This may be due to color sampling that occurs during scaling. Click the toggle to set imageSmoothingEnabled on the drawing context.</p> <canvas></canvas>请注意,要绘制您的“播放器”点,您可以选择手动执行相同的计算以避免由抗锯齿引起的模糊,或者如果您确实想要这种模糊,那么您可以简单地仅为该点设置变换。在你的位置上,我什至可能会在经过一定比例的回合后制作一些模块化的东西,并在下面平滑,但我会让读者去做那个实现。
n.5
// First calculate the scaled translations const scaled_offset_left = -OFFSET.x * scale + context.canvas.width * 0.5; const scaled_offset_top = -OFFSET.y * scale + context.canvas.height * 0.5; // when drawing each tile const dest_x = Math.floor( scaled_offset_left + (x * scale) ); const dest_y = Math.floor( scaled_offset_top + (y * scale) ); const dest_size = Math.ceil( TILE_SIZE * scale ); context.drawImage( source_image, frame.x, frame.y, TILE_SIZE, TILE_SIZE, dest_x, dest_y, dest_size, dest_size, );
/* This is the tile map. Each value is a frame index in the FRAMES array. Each frame tells drawImage where to blit the source from */ const MAP = [ 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 0, 1, 0, 1, 2, 2, 1, 2, 3, 2, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 3, 4, 3, 4, 5, 5, 4, 5, 6, 5, 3, 4, 3, 4, 5, 5, 4, 5, 6, 5, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 6, 7, 6, 7, 8, 8, 7, 8, 0, 8, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8 ]; const TILE_SIZE = 8; // Each tile is 8x8 pixels const MAP_HEIGHT = 80; // The map is 80 pixels tall by 80 pixels wide const MAP_WIDTH = 80; /* Each frame represents the source x, y coordinates of a tile in the source image. They are indexed according to the map values */ const FRAMES = [ { x:0, y:0 }, // map value = 0 { x:8, y:0 }, // map value = 1 { x:16, y:0 }, // map value = 2 { x:0, y:8 }, // etc. { x:8, y:8 }, { x:16, y:8}, { x:0, y:16}, { x:8, y:16}, { x:16, y:16} ]; /* These represent the state of the keyboard keys being used. false is up and true is down */ const KEYS = { down: false, left: false, right: false, scale_down: false, // the D key scale_up: false, // the F key up: false } /* This is the scroll offset. You can also think of it as the position of the red dot in the map. */ const OFFSET = { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.5 }; // It starts out centered in the map. const MAX_SCALE = 75; // Max scale is 75 times larger than the actual image size. const MIN_SCALE = 0; // Texture bleeding seems to only occur on upscale, but min scale is 0 in case you want to try it. var scale = 4.71; // some arbitrary number that will hopefully cause the issue in your browser /* Get the canvas drawing context. */ var context = document.querySelector('canvas').getContext('2d', { alpha: false, desynchronized: true }); /* The toggle button is the div */ var toggle = document.querySelector('div'); /* The source image is a 24x24 square with 9 tile images of various colors in it. */ var base_64_image_source = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAAKlnpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjazZhpciM7DoT/8xRzBO7LcbhGzA3m+POBVZK1Ws/dLzpasqUSiwsKCSSSVPN//13qP7y891b5kHIsMWpevvhiKxdZH6+6P432+/P4cbln7tvV9YalyfHtjp85nu2Tdkt/f7b3c55Ke7iZqF4WaPc36rmAzecCZ/tlIWeOBfQ5sWqX+/Z+5XZ2iCWn20cYZ/81z4t8/Cv58DHZaOKMzWJUXMnHbHVK0STmjzkG7mPt6rbI2HCufvtbbScn3DwCBjlrpzNO85nFSnf8V/4dn8Z5+miXuD7agzvdoYCMxbePDt9a/e3rnfXq0fwT+jtor1fmTTv+VbeQR3/ecA9Ixev3y3YTLhM9QLvxu1k51uvKd+2z6nUxTd1CKP9rjbw2tjxF9REY4vlQl0fZV/TDq97tUVFHhaMjXst8y7vwzqRGJ54GwdZ4d1OMBcplvBmmmmXm/u6mY6K30yarlbW2W7cbs0u22A6swCxvs2xyxQ2XAbsTEo5We7XF7GXLXq5Lrg09DF2tYTLDkF96q590Xkvyxhidr77CLiuRZ5URNzr5pBuImHU6NWwHX96PL8HVgSAxiWezLgrHtmOKFswXnbgNtKNj4PtIZJPGOQEuYumAMcaBgI7GqWCi0cnaZAyOzABUMd2SVA0ETAh2YKT1zkXAIRuKrgxJZne1wR7NRdkKEsFFEjGDUAUs7wPxk3wmhipZ6UMIZFDIoYQaXfQxxBhTFGatySWfQooqpZRTSTW77HPIMaecc8m12OJg3lBiSSWXUmplzcrMldGVDrU221zzLbTYkmq5lVY74dN9Dz321HMvvQ473IBbRhxp5FFGnWYSStPPMONMM88y6yLUllt+BbVggpVXWfWK2gnr0/sHqJkTNbuRko7pihqtKV2mMEInQTADMetNIPuFUA0BbQUznQ01SpATzHSxZEWwGBkEnGEEMRD009iwzBU7biqBztbfx02leMXN/g5ySqD7AXLPuL1CbdRd8NxGCDLYTtWO7KNPtZk/Kufrb3W5cJn5alkLfgotrZFYjSlm1itMgn+2sUxwa/qQW1wsPAJrazNbz7Gp2Zd1i6oSZu204y+3Rg+ptmONNrIbU781aLrVQ1hqWeNzW7PlLPOX2tr15ot78MLq0U0Qrk2sY/FgcvJkP8Km+TQpNV5+6LffCUP3ZZqmpxrv7qtXAygR33hjpDZtTavnZuIQP4ZVm5KLqt1IRYNxmT6tOXMbARypWjOtlusK5/Kp6dT6K5PVvghdIJNnJj0EMWP2AsbBNN/fJWdXJvDUTCZsM7EA0TEFi7piXr3rDUzVb7/F2KN/dyov3z8NmGa98MqdU6paCSjWKmT1EKQX9i3og9g3dTXXswwYwxBY5AJFthN1RqLOntYT99mqd8a0PcHx2MFS0sgzAtfbWGd/cVfd3O4CO2nXeQ6xkRCHTSjzgBImxaRkHTZS+vlbHRdGrN0DVk5mlvx2wNm/S+zfukTd+yTMI3N5ttvcvWbuiqmAfauW0IJIXHeFuCMhVTgWD7Obud5bnjZwWKCrDxu3LqGbha2y3FVvb5fMWGeWKzt/xUbIrObuTlKobrkvFlBv6MFh7zGANEEh6Q+8RkCm773yyGcM3Tkse4Azizel7aT9bVZjDnXLXbLKPbMJdT/ff8VuatPbj9gt72zTJQTm5qmOKFdtSpzH/SP7WqGMmYowx6APuHE5iNPi02C3UJYmcBr10Q34q8Myx7Tqft5/Nu0xJzBV+C+vFMgLNfjdLRXZyWDXxT1zle0fQ6hQ5Wvbq5YpgQdb7DKV4bfF465A7Sfi1KOVx6NTJw8jv0z8YKB6sPDWwFvzbo07TcPxYtxpmrra9mWab4wfoWoCz5tsxxzDAmtoRfZiIpZ6HPALrg+VHFqNclTWmB6CHimgETAnFSvhzf6hsxiUZNF3bDbDaqU6LKohoc2MyImAiImjjrE8+ojtUPU8gSWMcAazXycn4G1Ji/2ihTrgeqTEWjjRjG07vr50V9LfrIgSWq2XhQZqQ8AYreQ2J3/6zdD7kerXh96PVJ+HIvXqmEYPuCC1GpBlqdRRYEMRUchMDWcptmPFEwAJ1ZWggDX8JhUIgJlJXVdy1BXfxWqBcWhE2bRhWoO0Y+M2HJovDKWX26UOvkxH0euMLhJym31QzkOCps7N7mXlZo9kIndlXKmU2GpV69QCtGcrYO8gWklBA4vWNUfv4YjTGdP3k0Fs04QhnCiZtkwkitPYli1EwixFvCl5h6p2fc0joSfeuUHH9b4Z8jIXg8+5hHfantyIlZTWJvwK+9tOjq22LZ8WtYTOl/SRzTF5uPp2BJLWA21y7PF2Hpft+Ms8DL2diXkkmQ/nEpA3vpVBHmkRYUPs8EKibuGB7Qmd74LcuVuQ1K+ilB8gUnu/cwtQaweFD/KTbc4HCmdL0vbjKzOPAAwUI8qTFAQy/zUPyAaiidCbZeA3EffzIpTQR9O5Wd7LPJQFK9UiFBfgrjaIqQlfLACYHlUh6nUp5DaZdshLsghFFPJRG8EHJPInXTjG7q4abiGePtZ2J08tzl7W78KNpkEd4YPZ4NKksNh1sZufFE1xHVteyFtq6fKTYN3qzX5pbVOWf0JBXRsecr9mPID8hGrxuOzLnBMZ6kXaLFTCUcLBmt1g34KdJQ3qK/WPUM9DThKNheJ+L8bUszyE+F6CP/QOmSNebqMltzxRbIiyyj5ctv0E6pegyX1V/x6DrygA7RG8indxgtSUoG9VlHFCa+49jpU9WTz1ecZx7lkyPUo/xx5896dOIhE/BZHUwwN3dQ88wSHhcgTLbaiw+U5J5PxwjYTf6oonOiVxj0kN+14TH/hzcURAdY/lgV09NMNGhS1EdQjsvT02EiHtDDqYBQrRnzYAQhVx7uyXHUBaaQZKGAJXyh3qdUBPcBSISmRXtnuEO2Ur0QhfJ9G9lV1fGSWNGeWQJbT2NBSonufHTzEgccOVLSvTMBskxt5FXUz7bJl/MMvcGaWw6leMejJJhcOk3zQIMbreJJZLeddO60S0TTt6iYlYC95aT9RR8c0c7IiRbPQcajTZHc0kMSmys4aZUvY9kjFEQ4qLSufrJDiEAKsjHf2wiLTgKqKROsI0o6n7afJuvG/D9FSRi0xIUE6E5n4Y7wg3x8M3xMguR65PeSLRp+bS/NAqEtB8Nw0B+e9M47r6pWkec65oiO3JJ1TYEJeMbWxQ8Wvb7yIn2lp0to9y8G4d2y2BwfXUZN8v8pLqjfjRouhM6t26rJHPoVVCpcrmiL0ki7ewSaZIdIgkuegoKsJS3Q6/yQls/E6WdtElATpGkmZIefY215ZxsKhDtO5nhFwva2h1WYbdz+1CIrHSllj367SLZgni1Osa+OjdCj9dQL1b4acLqJ8+wp/10Qjn8WDX0HnV//TIBiOMuj3I0vrp2ObdgeuKpRya6jy4UdlcT25+eHCj9Xk2s09m1M3JzYvbLw9uXh3bGPVRqt0c24xvjm3+xWNoNhO3x9DuLzqGtj85hq5/6hi6xrtj6PSXHUP7Hx1D+z90DO0fjqH133YMnX9yDB3LHzmGLnfH0OavO4Y2n4+hSa6i/g9YSF5od4J2cQAAAAZiS0dEAP8AAAAAMyd88wAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QDDgsUN3w4Y2wAAAAbdEVYdENvbW1lbnQARnJhbmsgUG90aCB3YXMgaGVyZbBgrYoAAAA4SURBVDjLY/z//z8DVsBIkjADEwOVwKhBQ9EgFlzpoqGhEbtEfcNoYI8ahFG84CqORsujUYNIAADOzQexgePC2gAAAABJRU5ErkJggg=='; var source_image = new Image(); // This will be the source image /* The keyboard event handler */ function keyDownUp(event) { var state = event.type == 'keydown' ? true : false; switch (event.keyCode) { case 37: KEYS.left = state; break; case 38: KEYS.up = state; break; case 39: KEYS.right = state; break; case 40: KEYS.down = state; break; case 68: KEYS.scale_down = state; break; case 70: KEYS.scale_up = state; } } /* This is the update and rendering loop. It handles input and draws the images. */ function loop() { window.requestAnimationFrame(loop); // Perpetuate the loop /* Prepare to move and scale the image with the keyboard input */ if (KEYS.left) OFFSET.x -= 0.5; if (KEYS.right) OFFSET.x += 0.5; if (KEYS.up) OFFSET.y -= 0.5; if (KEYS.down) OFFSET.y += 0.5; if (KEYS.scale_down) scale -= 0.5 * scale / MAX_SCALE; if (KEYS.scale_up) scale += 0.5 * scale / MAX_SCALE; /* Keep the scale size within a defined range */ if (scale > MAX_SCALE) scale = MAX_SCALE; else if (scale < MIN_SCALE) scale = MIN_SCALE; /* Clear the canvas to gray. */ context.setTransform(1, 0, 0, 1, 0, 0); // Set the transform back to the identity matrix context.fillStyle = "#202830"; // Set the fill color to gray context.fillRect(0, 0, context.canvas.width, context.canvas.height); // fill the entire canvas /* [EDIT] Don't set the transform, we will calculate it ourselves // context.setTransform(scale, 0, 0, scale, -OFFSET.x * scale + context.canvas.width * 0.5, -OFFSET.y * scale + context.canvas.height * 0.5); First step is calculating the scaled translation */ const scaled_offset_left = -OFFSET.x * scale + context.canvas.width * 0.5; const scaled_offset_top = -OFFSET.y * scale + context.canvas.height * 0.5; let map_index = 0; // Track the tile index in the map. This increases once per draw loop. /* Loop through all tile positions in actual coordinate space so no additional calculations based on grid index are needed. */ for (let y = 0; y < MAP_HEIGHT; y += TILE_SIZE) { // y first so we draw rows from top to bottom for (let x = 0; x < MAP_WIDTH; x += TILE_SIZE) { const frame = FRAMES[MAP[map_index]]; // The frame is the source location of the tile in the source_image. /* [EDIT] We transform the coordinates ourselves We can control a uniform rounding by using floor and ceil */ const dest_x = Math.floor( scaled_offset_left + (x * scale) ); const dest_y = Math.floor( scaled_offset_top + (y * scale) ); const dest_size = Math.ceil(TILE_SIZE * scale); context.drawImage( source_image, frame.x, frame.y, TILE_SIZE, TILE_SIZE, dest_x, dest_y, dest_size, dest_size ); map_index++; } } /* Draw the red dot in the center of the screen. */ context.fillStyle = "#ff0000"; /* [EDIT] Do the same kind of calculations for the "dot" if you don't want antialiasing // const dot_x = Math.floor( scaled_offset_left + ((OFFSET.x - 0.5) * scale) ); // const dot_y = Math.floor( scaled_offset_top + ((OFFSET.y - 0.5) * scale) ); // const dot_size = Math.ceil( scale ); // context.fillRect( dot_x, dot_y, dot_size, dot_size ); // center on the dot But if you do want antialiasing for the dot, then just set the transformation for this drawing */ context.setTransform(scale, 0, 0, scale, scaled_offset_left, scaled_offset_top); context.fillRect( (OFFSET.x - 0.5), (OFFSET.y - 0.5), 1, 1 ); // center on the dot var smoothing = context.imageSmoothingEnabled; // Get the current smoothing value because we are going to ignore it briefly. /* Draw the source image in the top left corner for reference. */ context.setTransform(4, 0, 0, 4, 0, 0); // Zoom in on it so it's visible. context.imageSmoothingEnabled = false; // Set smoothing to false so we get a crisp source image representation (the real source image is not scaled at all). context.drawImage( source_image, 0, 0 ); context.imageSmoothingEnabled = smoothing; // Set smoothing back the way it was according to the toggle choice. } /* Turn image smoothing on and off when you press the toggle. */ function toggleSmoothing(event) { context.imageSmoothingEnabled = !context.imageSmoothingEnabled; if (context.imageSmoothingEnabled) toggle.innerText = 'Smoothing Enabled'; // Make sure the button has appropriate text in it. else toggle.innerText = 'Smoothing Disabled'; } /* The main loop will start after the source image is loaded to ensure there is something to draw. */ source_image.addEventListener('load', (event) => { window.requestAnimationFrame(loop); // Start the loop }, { once: true }); /* Add the toggle smoothing click handler to the div. */ toggle.addEventListener('click', toggleSmoothing); /* Add keyboard input */ window.addEventListener('keydown', keyDownUp); window.addEventListener('keyup', keyDownUp); /* Resize the canvas. */ context.canvas.width = 480; context.canvas.height = 480; toggleSmoothing(); // Set imageSmoothingEnabled /* Load the source image from the base64 string. */ source_image.setAttribute('src', base_64_image_source);
* { box-sizing: border-box; margin: 0; overflow: hidden; padding: 0; user-select: none; } body, html { background-color: #202830; color: #ffffff; height: 100%; width: 100%; } body { align-items: center; display: grid; justify-items: center; } p { max-width: 640px; } div { border: #ffffff 2px solid; bottom: 4px; cursor: pointer; padding: 8px; position: fixed; right: 4px }
<div>Smoothing Disabled</div> <p>Use the arrow keys to scroll and the D and F keys to scale. The source image is represented on the top left. Notice the vertical and horizontal lines that appear between tiles as you scroll and scale. They are the color of the tile's neighbor in the source image. This may be due to color sampling that occurs during scaling. Click the toggle to set imageSmoothingEnabled on the drawing context.</p> <canvas></canvas>
关于javascript - 使用 drawImage 从 Tile Sheet、Sprite Sheet 或 Texture Atlas 绘制多个图像时,如何防止纹理出血?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60684359/
相关文章:
javascript - 在 HTML Canvas 上加载图像时处理 Image.onerror 中的 404
javascript - 从 Sprite 图像生成动画?
java - 为复选框分配一个值(true 或 false)
javascript - 网页中的计时器与 phantomjs 中的时间同步
javascript - lightSlider 在缩略图旁边显示所有按钮
macos - OpenGL 纹理单元的范围是什么?
delphi - 在 TPNGImage 上拉伸(stretch)绘制
c++ - 变换所需的矩阵乘法图形矩阵?
arrays - 在另一个 Canvas 上创建 Canvas
用于检查输入值的 Javascript