Html HTML5 Canvas 相机/视口 - 如何实际操作?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/16919601/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
HTML5 Canvas camera/viewport - how to actually do it?
提问by user2337969
I'm sure this was solven 1000 times before: I got a canvas in the size of 960*560 and a room in the size of 5000*3000 of which always only 960*560 should be drawn, depending on where the player is. The player should be always in the middle, but when near to borders - then the best view should be calculated). The player can move entirely free with WASD or the arrow keys. And all objects should move themselves - instead of that i move everything else but the player to create the illusion that the player moves.
我确定这之前已经解决了 1000 次:我得到了一个大小为 960*560 的画布和一个大小为 5000*3000 的房间,其中始终只应绘制 960*560,具体取决于玩家所在的位置。玩家应该总是在中间,但是当靠近边界时 - 那么应该计算最佳视图)。玩家可以使用 WASD 或箭头键完全自由地移动。并且所有物体都应该自己移动 - 而不是我移动除玩家之外的所有其他东西,以创造玩家移动的错觉。
I now found those two quesitons:
我现在找到了这两个问题:
HTML5 - Creating a viewport for canvasworks, but only for this type of game, i can't reproduce the code for mine.
HTML5 - 为画布创建视口作品,但仅适用于这种类型的游戏,我无法重现我的代码。
Changing the view "center" of an html5 canvasseems to be more promising and also perfomant, but i only understand it for drawing all other objects correctly relative to the player and not how to scroll the canvas viewport relative to the player, which i want to achieve first of course.
更改 html5 画布的视图“中心”似乎更有前途且性能更好,但我只理解它相对于播放器正确绘制所有其他对象,而不是如何相对于播放器滚动画布视口,这是我想要的当然要先实现。
My code (simplified - the game logic is seperately):
我的代码(简化 - 游戏逻辑是分开的):
var canvas = document.getElementById("game");
canvas.tabIndex = 0;
canvas.focus();
var cc = canvas.getContext("2d");
// Define viewports for scrolling inside the canvas
/* Viewport x position */ view_xview = 0;
/* Viewport y position */ view_yview = 0;
/* Viewport width */ view_wview = 960;
/* Viewport height */ view_hview = 560;
/* Sector width */ room_width = 5000;
/* Sector height */ room_height = 3000;
canvas.width = view_wview;
canvas.height = view_hview;
function draw()
{
clear();
requestAnimFrame(draw);
// World's end and viewport
if (player.x < 20) player.x = 20;
if (player.y < 20) player.y = 20;
if (player.x > room_width-20) player.x = room_width-20;
if (player.y > room_height-20) player.y = room_height-20;
if (player.x > view_wview/2) ... ?
if (player.y > view_hview/2) ... ?
}
The way i am trying to get it working feels totally wrong and i don't even know how i am trying it... Any ideas? What do you think about the context.transform-thing?
我试图让它工作的方式感觉完全错误,我什至不知道我是如何尝试的......有什么想法吗?你如何看待 context.transform-thing?
I hope you understand my description and that someone has an idea. Kind regards
我希望你理解我的描述并且有人有想法。亲切的问候
回答by Gustavo Carvalho
LIVE DEMOat jsfiddle.net
现场演示在jsfiddle.net
This demo illustrates the viewport usage in a real game scenario. Use arrows keys to move the player over the room. The large room is generated on the fly using rectangles and the result is saved into an image.
此演示说明了真实游戏场景中的视口使用。使用箭头键在房间内移动玩家。大房间是使用矩形动态生成的,并将结果保存到图像中。
Notice that the player is always in the middle except when near to borders (as you desire).
请注意,玩家总是在中间,除非靠近边界(如您所愿)。
Now I'll try to explain the main portions of the code, at least the parts that are more difficult to understand just looking at it.
现在我将尝试解释代码的主要部分,至少是仅看代码更难理解的部分。
Using drawImage to draw large images according to viewport position
使用 drawImage 根据视口位置绘制大图
A variant of the drawImage method has eight new parameters. We can use this method to slice parts of a source image and draw them to the canvas.
drawImage 方法的一个变体有八个新参数。我们可以使用此方法对源图像的一部分进行切片并将它们绘制到画布上。
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
drawImage(图像,sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight)
The first parameter image, just as with the other variants, is either a reference to an image object or a reference to a different canvas element. For the other eight parameters it's best to look at the image below. The first four parameters define the location and size of the slice on the source image. The last four parameters define the position and size on the destination canvas.
与其他变体一样,第一个参数 image 要么是对图像对象的引用,要么是对不同画布元素的引用。对于其他八个参数,最好查看下图。前四个参数定义了源图像上切片的位置和大小。最后四个参数定义目标画布上的位置和大小。
Font: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Using_images
字体:https: //developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Using_images
How it works in demo:
在演示中它是如何工作的:
We have a large image that represents the room and we want to show on canvas only the part within the viewport. The crop position (sx, sy) is the same position of the camera (xView, yView) and the crop dimensions are the same as the viewport(canvas) so sWidth=canvas.width
and sHeight=canvas.height
.
我们有一个代表房间的大图像,我们只想在画布上显示视口内的部分。裁剪位置 (sx, sy) 与相机的位置 (xView, yView) 相同,裁剪尺寸与视口 (canvas) 相同,因此sWidth=canvas.width
和sHeight=canvas.height
。
We need to take care about the crop dimensions because drawImage
draws nothing on canvas if the crop position or crop dimensions based on position are invalid. That's why we need the if
sections bellow.
我们需要注意裁剪尺寸,因为drawImage
如果裁剪位置或基于位置的裁剪尺寸无效,则不会在画布上绘制任何内容。这就是为什么我们需要if
下面的部分。
var sx, sy, dx, dy;
var sWidth, sHeight, dWidth, dHeight;
// offset point to crop the image
sx = xView;
sy = yView;
// dimensions of cropped image
sWidth = context.canvas.width;
sHeight = context.canvas.height;
// if cropped image is smaller than canvas we need to change the source dimensions
if(image.width - sx < sWidth){
sWidth = image.width - sx;
}
if(image.height - sy < sHeight){
sHeight = image.height - sy;
}
// location on canvas to draw the croped image
dx = 0;
dy = 0;
// match destination with source to not scale the image
dWidth = sWidth;
dHeight = sHeight;
// draw the cropped image
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
Drawing game objects related to viewport
绘制与视口相关的游戏对象
When writing a game it's a good practice separate the logic and the rendering for each object in game. So in demo we have update
and draw
functions. The update
method changes object status like position on the "game world", apply physics, animation state, etc. The draw
method actually render the object and to render it properly considering the viewport, the object need to know the render context and the viewport properties.
在编写游戏时,最好将游戏中每个对象的逻辑和渲染分开。所以在演示中我们有update
和draw
函数。该update
方法更改对象状态,如“游戏世界”上的位置、应用物理、动画状态等。该draw
方法实际渲染对象并在考虑视口的情况下正确渲染它,对象需要知道渲染上下文和视口属性。
Notice that game objects are updated considering the game world's position. That means the (x,y) position of the object is the position in world. Despite of that, since the viewport is changing, objects need to be rendered properly and the render position will be different than world's position.
请注意,游戏对象会根据游戏世界的位置进行更新。这意味着对象的 (x,y) 位置是世界中的位置。尽管如此,由于视口正在发生变化,因此需要正确渲染对象,并且渲染位置将与世界位置不同。
The conversion is simple:
转换很简单:
object position in world(room): (x, y)
viewport position: (xView, yView)
世界(房间)中的物体位置:(x, y)
视口位置:(xView, yView)
render position: (x-xView, y-yView)
渲染位置:(x-xView, y-yView)
This works for all kind of coordinates, even the negative ones.
这适用于所有类型的坐标,甚至是负坐标。
Game Camera
游戏相机
Our game objects have a separated update method. In Demo implementation, the camera is treated as a game object and also have a separated update method.
我们的游戏对象有一个单独的更新方法。在 Demo 实现中,相机被视为一个游戏对象,也有一个单独的更新方法。
The camera object holds the left top position of viewport (xView, yView)
, an object to be followed, a rectangle representing the viewport, a rectangle that represents the game world's boundary and the minimal distance of each border that player could be before camera starts move (xDeadZone, yDeadZone). Also we defined the camera's degrees of freedom (axis). For top view style games, like RPG, the camera is allowed to move in both x(horizontal) and y(vertical) axis.
相机对象持有 viewport 的左上角位置(xView, yView)
,一个要跟随的对象,一个代表视口的矩形,一个代表游戏世界边界的矩形以及玩家在相机开始移动之前每个边界的最小距离(xDeadZone, yDeadZone )。我们还定义了相机的自由度(轴)。对于顶视图风格的游戏,如 RPG,允许相机在 x(水平)和 y(垂直)轴上移动。
To keep player in the middle of viewport we set the deadZone of each axis to converge with the center of canvas. Look at the follow function in the code:
为了让玩家保持在视口中间,我们将每个轴的死区设置为与画布中心会聚。查看代码中的follow函数:
camera.follow(player, canvas.width/2, canvas.height/2)
camera.follow(播放器,canvas.width/2,canvas.height/2)
Note: See the UPDATE section below as this will not produce the expected behavior when any dimension of the map (room) is smaller than canvas.
注意:请参阅下面的更新部分,因为当地图(房间)的任何尺寸小于画布时,这不会产生预期的行为。
World's limits
世界的极限
Since each object, including camera, have its own update function, its easy to check the game world's boundary. Only remember to put the code that block the movement at the final of the update function.
由于包括相机在内的每个对象都有自己的更新功能,因此很容易检查游戏世界的边界。只记得将阻止移动的代码放在更新函数的最后。
Demonstration
示范
See the full code and try it yourself. Most parts of the code have comments that guide you through. I'll assume that you know the basics of Javascript and how to work with prototypes (sometimes I use the term "class" for a prototype object just because it have a similar behavior of a Class in languages like Java).
查看完整代码并自己尝试。代码的大多数部分都有指导您完成的注释。我假设您了解 Javascript 的基础知识以及如何使用原型(有时我将术语“类”用于原型对象,只是因为它与 Java 等语言中的类具有类似的行为)。
Full code:
完整代码:
<!DOCTYPE HTML>
<html>
<body>
<canvas id="gameCanvas" width=400 height=400 />
<script>
// wrapper for our game "classes", "methods" and "objects"
window.Game = {};
// wrapper for "class" Rectangle
(function() {
function Rectangle(left, top, width, height) {
this.left = left || 0;
this.top = top || 0;
this.width = width || 0;
this.height = height || 0;
this.right = this.left + this.width;
this.bottom = this.top + this.height;
}
Rectangle.prototype.set = function(left, top, /*optional*/ width, /*optional*/ height) {
this.left = left;
this.top = top;
this.width = width || this.width;
this.height = height || this.height
this.right = (this.left + this.width);
this.bottom = (this.top + this.height);
}
Rectangle.prototype.within = function(r) {
return (r.left <= this.left &&
r.right >= this.right &&
r.top <= this.top &&
r.bottom >= this.bottom);
}
Rectangle.prototype.overlaps = function(r) {
return (this.left < r.right &&
r.left < this.right &&
this.top < r.bottom &&
r.top < this.bottom);
}
// add "class" Rectangle to our Game object
Game.Rectangle = Rectangle;
})();
// wrapper for "class" Camera (avoid global objects)
(function() {
// possibles axis to move the camera
var AXIS = {
NONE: 1,
HORIZONTAL: 2,
VERTICAL: 3,
BOTH: 4
};
// Camera constructor
function Camera(xView, yView, viewportWidth, viewportHeight, worldWidth, worldHeight) {
// position of camera (left-top coordinate)
this.xView = xView || 0;
this.yView = yView || 0;
// distance from followed object to border before camera starts move
this.xDeadZone = 0; // min distance to horizontal borders
this.yDeadZone = 0; // min distance to vertical borders
// viewport dimensions
this.wView = viewportWidth;
this.hView = viewportHeight;
// allow camera to move in vertical and horizontal axis
this.axis = AXIS.BOTH;
// object that should be followed
this.followed = null;
// rectangle that represents the viewport
this.viewportRect = new Game.Rectangle(this.xView, this.yView, this.wView, this.hView);
// rectangle that represents the world's boundary (room's boundary)
this.worldRect = new Game.Rectangle(0, 0, worldWidth, worldHeight);
}
// gameObject needs to have "x" and "y" properties (as world(or room) position)
Camera.prototype.follow = function(gameObject, xDeadZone, yDeadZone) {
this.followed = gameObject;
this.xDeadZone = xDeadZone;
this.yDeadZone = yDeadZone;
}
Camera.prototype.update = function() {
// keep following the player (or other desired object)
if (this.followed != null) {
if (this.axis == AXIS.HORIZONTAL || this.axis == AXIS.BOTH) {
// moves camera on horizontal axis based on followed object position
if (this.followed.x - this.xView + this.xDeadZone > this.wView)
this.xView = this.followed.x - (this.wView - this.xDeadZone);
else if (this.followed.x - this.xDeadZone < this.xView)
this.xView = this.followed.x - this.xDeadZone;
}
if (this.axis == AXIS.VERTICAL || this.axis == AXIS.BOTH) {
// moves camera on vertical axis based on followed object position
if (this.followed.y - this.yView + this.yDeadZone > this.hView)
this.yView = this.followed.y - (this.hView - this.yDeadZone);
else if (this.followed.y - this.yDeadZone < this.yView)
this.yView = this.followed.y - this.yDeadZone;
}
}
// update viewportRect
this.viewportRect.set(this.xView, this.yView);
// don't let camera leaves the world's boundary
if (!this.viewportRect.within(this.worldRect)) {
if (this.viewportRect.left < this.worldRect.left)
this.xView = this.worldRect.left;
if (this.viewportRect.top < this.worldRect.top)
this.yView = this.worldRect.top;
if (this.viewportRect.right > this.worldRect.right)
this.xView = this.worldRect.right - this.wView;
if (this.viewportRect.bottom > this.worldRect.bottom)
this.yView = this.worldRect.bottom - this.hView;
}
}
// add "class" Camera to our Game object
Game.Camera = Camera;
})();
// wrapper for "class" Player
(function() {
function Player(x, y) {
// (x, y) = center of object
// ATTENTION:
// it represents the player position on the world(room), not the canvas position
this.x = x;
this.y = y;
// move speed in pixels per second
this.speed = 200;
// render properties
this.width = 50;
this.height = 50;
}
Player.prototype.update = function(step, worldWidth, worldHeight) {
// parameter step is the time between frames ( in seconds )
// check controls and move the player accordingly
if (Game.controls.left)
this.x -= this.speed * step;
if (Game.controls.up)
this.y -= this.speed * step;
if (Game.controls.right)
this.x += this.speed * step;
if (Game.controls.down)
this.y += this.speed * step;
// don't let player leaves the world's boundary
if (this.x - this.width / 2 < 0) {
this.x = this.width / 2;
}
if (this.y - this.height / 2 < 0) {
this.y = this.height / 2;
}
if (this.x + this.width / 2 > worldWidth) {
this.x = worldWidth - this.width / 2;
}
if (this.y + this.height / 2 > worldHeight) {
this.y = worldHeight - this.height / 2;
}
}
Player.prototype.draw = function(context, xView, yView) {
// draw a simple rectangle shape as our player model
context.save();
context.fillStyle = "black";
// before draw we need to convert player world's position to canvas position
context.fillRect((this.x - this.width / 2) - xView, (this.y - this.height / 2) - yView, this.width, this.height);
context.restore();
}
// add "class" Player to our Game object
Game.Player = Player;
})();
// wrapper for "class" Map
(function() {
function Map(width, height) {
// map dimensions
this.width = width;
this.height = height;
// map texture
this.image = null;
}
// creates a prodedural generated map (you can use an image instead)
Map.prototype.generate = function() {
var ctx = document.createElement("canvas").getContext("2d");
ctx.canvas.width = this.width;
ctx.canvas.height = this.height;
var rows = ~~(this.width / 44) + 1;
var columns = ~~(this.height / 44) + 1;
var color = "red";
ctx.save();
ctx.fillStyle = "red";
for (var x = 0, i = 0; i < rows; x += 44, i++) {
ctx.beginPath();
for (var y = 0, j = 0; j < columns; y += 44, j++) {
ctx.rect(x, y, 40, 40);
}
color = (color == "red" ? "blue" : "red");
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
}
ctx.restore();
// store the generate map as this image texture
this.image = new Image();
this.image.src = ctx.canvas.toDataURL("image/png");
// clear context
ctx = null;
}
// draw the map adjusted to camera
Map.prototype.draw = function(context, xView, yView) {
// easiest way: draw the entire map changing only the destination coordinate in canvas
// canvas will cull the image by itself (no performance gaps -> in hardware accelerated environments, at least)
/*context.drawImage(this.image, 0, 0, this.image.width, this.image.height, -xView, -yView, this.image.width, this.image.height);*/
// didactic way ( "s" is for "source" and "d" is for "destination" in the variable names):
var sx, sy, dx, dy;
var sWidth, sHeight, dWidth, dHeight;
// offset point to crop the image
sx = xView;
sy = yView;
// dimensions of cropped image
sWidth = context.canvas.width;
sHeight = context.canvas.height;
// if cropped image is smaller than canvas we need to change the source dimensions
if (this.image.width - sx < sWidth) {
sWidth = this.image.width - sx;
}
if (this.image.height - sy < sHeight) {
sHeight = this.image.height - sy;
}
// location on canvas to draw the croped image
dx = 0;
dy = 0;
// match destination with source to not scale the image
dWidth = sWidth;
dHeight = sHeight;
context.drawImage(this.image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
}
// add "class" Map to our Game object
Game.Map = Map;
})();
// Game Script
(function() {
// prepaire our game canvas
var canvas = document.getElementById("gameCanvas");
var context = canvas.getContext("2d");
// game settings:
var FPS = 30;
var INTERVAL = 1000 / FPS; // milliseconds
var STEP = INTERVAL / 1000 // seconds
// setup an object that represents the room
var room = {
width: 500,
height: 300,
map: new Game.Map(500, 300)
};
// generate a large image texture for the room
room.map.generate();
// setup player
var player = new Game.Player(50, 50);
// Old camera setup. It not works with maps smaller than canvas. Keeping the code deactivated here as reference.
/* var camera = new Game.Camera(0, 0, canvas.width, canvas.height, room.width, room.height);*/
/* camera.follow(player, canvas.width / 2, canvas.height / 2); */
// Set the right viewport size for the camera
var vWidth = Math.min(room.width, canvas.width);
var vHeight = Math.min(room.height, canvas.height);
// Setup the camera
var camera = new Game.Camera(0, 0, vWidth, vHeight, room.width, room.height);
camera.follow(player, vWidth / 2, vHeight / 2);
// Game update function
var update = function() {
player.update(STEP, room.width, room.height);
camera.update();
}
// Game draw function
var draw = function() {
// clear the entire canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// redraw all objects
room.map.draw(context, camera.xView, camera.yView);
player.draw(context, camera.xView, camera.yView);
}
// Game Loop
var gameLoop = function() {
update();
draw();
}
// <-- configure play/pause capabilities:
// Using setInterval instead of requestAnimationFrame for better cross browser support,
// but it's easy to change to a requestAnimationFrame polyfill.
var runningId = -1;
Game.play = function() {
if (runningId == -1) {
runningId = setInterval(function() {
gameLoop();
}, INTERVAL);
console.log("play");
}
}
Game.togglePause = function() {
if (runningId == -1) {
Game.play();
} else {
clearInterval(runningId);
runningId = -1;
console.log("paused");
}
}
// -->
})();
// <-- configure Game controls:
Game.controls = {
left: false,
up: false,
right: false,
down: false,
};
window.addEventListener("keydown", function(e) {
switch (e.keyCode) {
case 37: // left arrow
Game.controls.left = true;
break;
case 38: // up arrow
Game.controls.up = true;
break;
case 39: // right arrow
Game.controls.right = true;
break;
case 40: // down arrow
Game.controls.down = true;
break;
}
}, false);
window.addEventListener("keyup", function(e) {
switch (e.keyCode) {
case 37: // left arrow
Game.controls.left = false;
break;
case 38: // up arrow
Game.controls.up = false;
break;
case 39: // right arrow
Game.controls.right = false;
break;
case 40: // down arrow
Game.controls.down = false;
break;
case 80: // key P pauses the game
Game.togglePause();
break;
}
}, false);
// -->
// start the game when page is loaded
window.onload = function() {
Game.play();
}
</script>
</body>
</html>
UPDATE
更新
If width and/or height of the map (room) is smaller than canvas the previous code will not work properly. To resolve this, in the Game Script make the setup of the camera as followed:
如果地图(房间)的宽度和/或高度小于画布,则之前的代码将无法正常工作。为了解决这个问题,在游戏脚本中设置相机如下:
// Set the right viewport size for the camera
var vWidth = Math.min(room.width, canvas.width);
var vHeight = Math.min(room.height, canvas.height);
var camera = new Game.Camera(0, 0, vWidth, vHeight, room.width, room.height);
camera.follow(player, vWidth / 2, vHeight / 2);
You just need to tell the camera constructor that viewport will be the smallest value between map (room) or canvas. And since we want the player centered and bonded to that viewport, the camera.follow
function must be update as well.
您只需要告诉相机构造函数视口将是地图(房间)或画布之间的最小值。由于我们希望玩家居中并绑定到该视口,因此该camera.follow
函数也必须更新。
Feel free to report any errors or to add suggestions.
随时报告任何错误或添加建议。
回答by Colton
The code in the accepted answer is a bit much. Its this simple:
接受的答案中的代码有点多。就这么简单:
function draw() {
ctx.setTransform(1,0,0,1,0,0);//reset the transform matrix as it is cumulative
ctx.clearRect(0, 0, canvas.width, canvas.height);//clear the viewport AFTER the matrix is reset
//Clamp the camera position to the world bounds while centering the camera around the player
var camX = clamp(-player.x + canvas.width/2, yourWorld.minX, yourWorld.maxX - canvas.width);
var camY = clamp(-player.y + canvas.height/2, yourWorld.minY, yourWorld.maxY - canvas.height);
ctx.translate( camX, camY );
//Draw everything
}
And clamp looks like:
钳子看起来像:
function clamp(value, min, max){
if(value < min) return min;
else if(value > max) return max;
return value;
}
回答by markE
Here's how to use canvas to be a viewport on another larger-than-canvas image
以下是如何使用画布作为另一个大于画布图像的视口
A viewport is really just a cropped portion of a larger image that is displayed to the user.
视口实际上只是显示给用户的较大图像的裁剪部分。
In this case, the viewport will be displayed to the user on a canvas (the canvas is the viewport).
在这种情况下,视口将在画布上显示给用户(画布就是视口)。
First, code a move function that pans the viewport around the larger image.
首先,编写一个移动函数,围绕较大的图像平移视口。
This function moves the top/left corner of the viewport by 5px in the specified direction:
此函数将视口的上/左角向指定方向移动 5px:
function move(direction){
switch (direction){
case "left":
left-=5;
break;
case "up":
top-=5;
break;
case "right":
left+=5;
break;
case "down":
top+=5
break;
}
draw(top,left);
}
The move function calls the draw function.
move函数调用draw函数。
In draw(), the drawImage
function will crop a specified portion of a larger image.
在 draw() 中,该drawImage
函数将裁剪较大图像的指定部分。
drawImage
will also display that “cropped background” to the user on the canvas.
drawImage
还将在画布上向用户显示“裁剪的背景”。
context.clearRect(0,0,game.width,game.height);
context.drawImage(background,cropLeft,cropTop,cropWidth,cropHeight,
0,0,viewWidth,viewHeight);
In this example,
在这个例子中,
Background is the full background image (usually not displayed but is rather a source for cropping)
背景是完整的背景图像(通常不显示,而是裁剪的来源)
cropLeft & cropTop define where on the background image the cropping will begin.
cropLeft 和cropTop 定义了裁剪将在背景图像上的哪个位置开始。
cropWidth & cropHeight define how large a rectangle will be cropped from the background image.
cropWidth 和cropHeight 定义了从背景图像中裁剪出的矩形的大小。
0,0 say that the sub-image that has been cropped from the background will be drawn at 0,0 on the viewport canvas.
0,0 表示从背景裁剪的子图像将在视口画布上的 0,0 处绘制。
viewWidth & viewHeight are the width and height of the viewport canvas
viewWidth & viewHeight 是视口画布的宽度和高度
So here is an example of drawImage using numbers.
所以这是一个使用数字的 drawImage 示例。
Let's say our viewport (= our display canvas) is 150 pixels wide and 100 pixels high.
假设我们的视口(=我们的显示画布)宽 150 像素,高 100 像素。
context.drawImage(background,75,50,150,100,0,0,150,100);
The 75 & 50 say that cropping will start at position x=75/y=50 on the background image.
75 和 50 表示裁剪将从背景图像上的位置 x=75/y=50 开始。
The 150,100 say that the rectangle to be cropped will be 150 wide and 100 high.
150,100 表示要裁剪的矩形将是 150 宽和 100 高。
The 0,0,150,100 say that the cropped rectangle image will be displayed using the full size of the viewport canvas.
0,0,150,100 表示将使用视口画布的全尺寸显示裁剪的矩形图像。
That's it for the mechanics of drawing a viewport…just add key-controls!
这就是绘制视口的机制……只需添加按键控制!
Here is code and a Fiddle: http://jsfiddle.net/m1erickson/vXqyc/
这是代码和小提琴:http: //jsfiddle.net/m1erickson/vXqyc/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var game=document.getElementById("game");
var gameCtx=game.getContext("2d");
var left=20;
var top=20;
var background=new Image();
background.onload=function(){
canvas.width=background.width/2;
canvas.height=background.height/2;
gameCtx.fillStyle="red";
gameCtx.strokeStyle="blue";
gameCtx.lineWidth=3;
ctx.fillStyle="red";
ctx.strokeStyle="blue";
ctx.lineWidth=3;
move(top,left);
}
background.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/game.jpg";
function move(direction){
switch (direction){
case "left":
left-=5;
break;
case "up":
top-=5;
break;
case "right":
left+=5;
break;
case "down":
top+=5
break;
}
draw(top,left);
}
function draw(top,left){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(background,0,0,background.width,background.height,0,0,canvas.width,canvas.height);
gameCtx.clearRect(0,0,game.width,game.height);
gameCtx.drawImage(background,left,top,250,150,0,0,250,150);
gameCtx.beginPath();
gameCtx.arc(125,75,10,0,Math.PI*2,false);
gameCtx.closePath();
gameCtx.fill();
gameCtx.stroke();
ctx.beginPath();
ctx.rect(left/2,top/2,125,75);
ctx.stroke();
ctx.beginPath();
ctx.arc(left/2+125/2,top/2+75/2,5,0,Math.PI*2,false);
ctx.stroke();
ctx.fill();
}
$("#moveLeft").click(function(){move("left");});
$("#moveRight").click(function(){move("right");});
$("#moveUp").click(function(){move("up");});
$("#moveDown").click(function(){move("down");});
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="game" width=250 height=150></canvas><br>
<canvas id="canvas" width=500 height=300></canvas><br>
<button id="moveLeft">Left</button>
<button id="moveRight">Right</button>
<button id="moveUp">Up</button>
<button id="moveDown">Down</button>
</body>
</html>
回答by Bjorn 'Bjeaurn' S
The way you're going about it right now seems correct to me. I would change the "20" bounds to a variable though, so you can easily change the bounds of a level or the entire game if you ever require so.
你现在的做法对我来说似乎是正确的。不过,我会将“20”边界更改为变量,因此您可以根据需要轻松更改关卡或整个游戏的边界。
You could abstract this logic into a specific "Viewport" method, that would simply handle the calculations required to determine where your "Camera" needs to be on the map, and then make sure the X and Y coordinates of your character match the center of your camera.
您可以将此逻辑抽象为特定的“视口”方法,该方法将简单地处理确定“相机”需要在地图上的位置所需的计算,然后确保角色的 X 和 Y 坐标与中心匹配你的相机。
You could also flip that method and determine the location of your camera based on the characters position (e.g.: (position.x - (desired_camera_size.width / 2))
) and draw the camera from there on out.
您还可以翻转该方法并根据字符位置(例如:)确定相机的位置,(position.x - (desired_camera_size.width / 2))
然后从那里开始绘制相机。
When you have your camera position figured out, you can start worrying about drawing the room itself as the first layer of your canvas.
当您确定了相机位置后,您就可以开始担心将房间本身绘制为画布的第一层。
回答by ggorlen
This is a simple matter of setting the viewport to the target's x and y coordinates, as Colton states, on each frame. Transforms are not necessary but can be used as desired. The formula that worked for me was:
这是将视口设置为目标的 x 和 y 坐标的简单问题,正如科尔顿所说,在每一帧上。转换不是必需的,但可以根据需要使用。对我有用的公式是:
function update() {
// Assign the viewport to follow a target for this frame
viewport.x = -target.x + canvas.width / 2;
viewport.y = -target.y + canvas.height / 2;
// Draw each entity, including the target, relative to the viewport
ctx.fillRect(
entity.x + viewport.x,
entity.y + viewport.y,
entity.size,
entity.size
);
}
Clamping to the map is an optional second step:
固定到地图是可选的第二步:
function update() {
// Assign the viewport to follow a target for this frame
viewport.x = -target.x + canvas.width / 2;
viewport.y = -target.y + canvas.height / 2;
// Keep viewport in map bounds
viewport.x = clamp(viewport.x, canvas.width - map.width, 0);
viewport.y = clamp(viewport.y, canvas.height - map.height, 0);
// Draw each entity, including the target, relative to the viewport
ctx.fillRect(
entity.x + viewport.x,
entity.y + viewport.y,
entity.size,
entity.size
);
}
// Restrict n to a range between lo and hi
const clamp = (n, lo, hi) => n < lo ? lo : n > hi ? hi : n;
Here's an example: https://jsfiddle.net/ggorlen/7yv7u572/
回答by Alex
@gustavo-carvalho's solution is phenomenal, but it involves extensive calculations and cognitive overhead. @Colton's approach is a step in the right direction; too bad it wasn't elaborated enough in his answer. I took his idea and ran with it to create this CodePen. It achieves exactly what @user2337969 is asking for using context.translate
. The beauty is that this doesn't require offsetting any map or player coordinates so drawing them is as easy as using their x
and y
directly, which is much more straightforward.
@gustavo-carvalho 的解决方案非常出色,但它涉及大量计算和认知开销。@Colton 的方法是朝着正确方向迈出的一步;太糟糕了,他的回答没有详细说明。我接受了他的想法并用它来创建这个 CodePen。它完全实现了@user2337969 要求使用context.translate
. 妙处在于,这并不需要任何抵消地图或玩家坐标,以便绘制他们是那么容易,因为使用他们x
和y
直接,这是更简单。
Think of the 2D camera as a rectangle that pans inside a larger map. Its top-left corner is at (x, y)
coordinates in the map, and its size is that of the canvas, i.e. canvas.width
and canvas.height
. That means that x
can range from 0
to map.width - canvas.width
, and y
from 0
to map.height - canvas.height
(inclusive). These are min
and max
that we feed into @Colton's clamp
method.
将 2D 相机视为在较大地图内平移的矩形。它的左上角位于(x, y)
地图中的坐标处,其大小是画布的大小,即canvas.width
和canvas.height
。这意味着x
范围可以从0
到map.width - canvas.width
,y
从0
到map.height - canvas.height
(包括)。这些都是min
和max
我们送入@科尔顿的clamp
方法。
To make it work however, I had to flip the sign on x
and y
since with context.translate
, positive values shift the canvas to the right (making an illusion as if the camera pans to the left) and negative - to the left (as if the camera pans to the right).
然而,为了让它工作,我不得不打开标志x
,y
因为context.translate
,正值将画布向右移动(产生一种错觉,好像相机向左平移)和负值 - 向左(好像相机平移向右)。
回答by J.M.I. MADISON
Save the code below as a .HTM (.html) file and open in your browser.
将下面的代码另存为 .HTM (.html) 文件并在浏览器中打开。
The result should match this screen shot EXACTLY.
结果应该与此屏幕截图完全匹配。
Here is some example code that maps viewports of different sizes onto each other. Though this implementation uses pixels, you could expand upon this logic to render tiles. I actually store my tilemaps as .PNG files. Depending on the color of the pixel, it can represent a different tile type. The code here is designed to sample from viewports 1,2, or 3 and paste results into viewport 0.
下面是一些将不同大小的视口相互映射的示例代码。尽管此实现使用像素,但您可以扩展此逻辑以渲染图块。我实际上将我的 tilemaps 存储为 .PNG 文件。根据像素的颜色,它可以代表不同的图块类型。此处的代码旨在从视口 1、2 或 3 中采样并将结果粘贴到视口 0 中。
Youtube Video Playlist For The Screenshot and Code Directly Below : REC_MAP
屏幕截图和代码的 Youtube 视频播放列表直接在下面:REC_MAP
EDIT: REC_MAP.HTM CODE MOVED TO PASTEBIN: https://pastebin.com/9hWs8Bag
编辑:REC_MAP.HTM 代码移至 Pastebin:https://pastebin.com/9hWs8Bag
Part #2: BUF_VEW.HTM (Sampling from off screen buffer) We are going to refactor the code from the previous demo so that our source viewport samples a bitmap that is off screen. Eventually we will interpret each pixel color on the bitmap as a unique tile value. We don't go that far in this code, this is just a refactor to get one of our viewports off-screen. I recorded the entire process here. No edits. Entire process including me taking way too long to think up variable names.
第 2 部分:BUF_VEW.HTM(从屏幕外缓冲区采样) 我们将重构上一个演示中的代码,以便我们的源视口对屏幕外的位图进行采样。最终,我们将位图上的每个像素颜色解释为唯一的图块值。我们在这段代码中没有走那么远,这只是为了让我们的一个视口离开屏幕的重构。我在这里记录了整个过程。没有编辑。包括我在内的整个过程花费了太长时间来想出变量名称。
Youtube Video Playlist For The Screenshot and Code Directly Below : BUF_VEW
屏幕截图和代码的 Youtube 视频播放列表直接在下面:BUF_VEW
As before, you can take this source code, save it as a .HTM (.html) file, and run it in your browser.
和以前一样,您可以使用此源代码,将其另存为 .HTM (.html) 文件,然后在浏览器中运行它。
EDIT: BUF_VEW.HTM CODE MOVED TO PASTEBIN: https://pastebin.com/zedhD60u
编辑:BUF_VEW.HTM 代码移至 Pastebin:https://pastebin.com/zedhD60u
Part #3: UIN_ADA.HTM ( User Input Adapter & Snapping Camera ) We are now going to edit the previous BUF_VEW.HTM file from part #2 and add 2 new pieces of functionality.
第 3 部分:UIN_ADA.HTM(用户输入适配器和捕捉相机)我们现在将编辑第 2 部分中先前的 BUF_VEW.HTM 文件并添加 2 个新功能。
1: User input handling
1:用户输入处理
2: A camera that can zoom in and out and be moved.
2:可以放大缩小和移动的相机。
This camera will move in increments of it's own viewport selection area width and height, meaning the motion will be very "snappy". This camera is designed for level editing, not really in-game play. We are focusing on a level editor camera first. The long-term end goal is to make the editor-code and the in-game-play code the same code. The only difference should be that when in game-play mode the camera will behave differently and tile-map editing will be disabled.
该相机将以它自己的视口选择区域的宽度和高度为增量移动,这意味着运动将非常“快速”。该相机专为关卡编辑而设计,并非真正用于游戏中。我们首先专注于关卡编辑器相机。长期的最终目标是使编辑器代码和游戏中的代码相同。唯一的区别应该是在游戏模式下,相机的行为会有所不同,并且将禁用平铺地图编辑。
Youtube Video Playlist For The Screenshot And Code Directly Below: UIN_ADA
屏幕截图和代码的 Youtube 视频播放列表直接在下面:UIN_ADA
Copy code below, save as: "UIN_ADA.HTM" and run in browser.
复制下面的代码,另存为:“UIN_ADA.HTM”并在浏览器中运行。
Controls: Arrows & "+" "-" for camera zoom-in, zoom-out.
控制:箭头和“+”“-”用于相机放大、缩小。
EDIT: UIN_ADA.HTM MOVED TO PASTEBIN: https://pastebin.com/ntmWihra
编辑:UIN_ADA.HTM 移至 Pastebin:https://pastebin.com/ntmWihra
Part #4: DAS_BOR.HTM ( DAShed_BOaRders ) We are going to do some calculations to draw a 1 pixel thin boarder around each tile. The result won't be fancy, but it will help us verify that we are able to get the local coordinates of each tile and do something useful with them. These tile-local coordinates will be necessary for mapping a bitmap image onto the tile in later installments.
第 4 部分:DAS_BOR.HTM (DAShed_BOaRders) 我们将进行一些计算以在每个图块周围绘制一个 1 像素的薄边框。结果不会很花哨,但它会帮助我们验证我们是否能够获得每个瓦片的本地坐标并对其做一些有用的事情。在后面的部分中,将位图图像映射到图块上时需要这些图块局部坐标。
Youtube_Playlist: DAS_BOR.HTM Source_Code: DAS_BOR.HTM
Youtube_Playlist: DAS_BOR.HTM Source_Code: DAS_BOR.HTM
Part #5: Zoom + Pan over WebGL Canvas fragment shader code: This is the math required for zooming and panning over a shader written in GLSL. Rather than taking a sub-sample of off-screen data, we take a sub-sample of the gl_FragCoord values. The math here allows for an inset on-screen viewport and a camera that can zoom and pan over your shader. If you have done a shader tutorial by "Lewis Lepton" and you would like to zoom and pan over it, you can filter his input coordinates through this logic and that should do it.
第 5 部分:缩放 + 平移 WebGL 画布片段着色器代码:这是缩放和平移用 GLSL 编写的着色器所需的数学运算。我们不是采用屏幕外数据的子样本,而是采用 gl_FragCoord 值的子样本。这里的数学允许插入屏幕上的视口和可以缩放和平移着色器的相机。如果您已经完成了“Lewis Lepton”的着色器教程,并且您想对其进行缩放和平移,您可以通过此逻辑过滤他的输入坐标,这应该可以做到。
Quick Video Explanation Of Code
//|StackOverflow Says:
//|Links to pastebin.com must be accompanied by code. Please |//
//|indent all code by 4 spaces using the code toolbar button |//
//|or the CTRL+K keyboard shortcut. For more editing help, |//
//|click the [?] toolbar icon. |//
//| |//
//|StackOverflow Also Says (when I include the code here) |//
//|You are over you 30,000 character limit for posts. |//
function(){ console.log("[FixingStackOverflowComplaint]"); }