github.com/victorqribeiro/isocity
一个简单的开源项目,让你手动建造小城市。
另外,我还在这里看到一个类似的。更复杂一些,可以进行地图浏览(移动)
它只有四个文件,css、js、图片、html,而且代码也不多,值得啃一啃,用在自己的项目中。
有一段时间很想做一个以本地地图为背景的多人智力游戏,但终因缺少美工等问题而搁置,将以上思路结合应用,或许可以做一个实时地图生成(先用此项目生成全景地图,然后转换为代码保存)
运行时,你可以看到地址栏的变化类似
http://127.0.0.1:8080/#Li4ALg9DAAAuAC4DAAEpAhcCCQICAy4ALg86OgMuAC4DADoDLi4uDzo6CAICAgklOg==
其中#号后是经过Base64编码的地图信息
在CSS文件中,#tools > div 部份定义了背景图片,及每个小图大小。
以下代码已有修改
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="theme-color" content="#FFF"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta property="og:image:width" content="512" />
<meta property="og:image:height" content="512" />
<link rel="icon" href="favicon.png" sizes="256x256"/>
<link rel="apple-touch-icon" href="favicon.png" />
<title> IsoCity </title>
<link rel="stylesheet" type="text/css" href="main.css" />
</head>
<body>
<section id="main">
<div id="tools"></div>
<div id="area">
<canvas id="bg"></canvas>
<canvas id="fg"></canvas>
</div>
</section>
<script src="main.js"></script>
</body>
</html>
const $ = _ => document.querySelector(_)
const $c = _ => document.createElement(_)
let canvas, bg, fg, cf, ntiles, tileWidth, tileHeight, map, tools, tool, activeTool, isPlacing
/* texture from https://opengameart.org/content/isometric-landscape */
const texture = new Image()
texture.src = "textures/01_130x66_130x230.png"
texture.onload = _ => init()
const init = function(){
tool = [0,0]
map = [
[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]]
]
canvas = $("#bg")
canvas.width = 910
canvas.height = 666
w = 910
h = 462
texWidth = 12
texHeight = 6
bg = canvas.getContext("2d")
ntiles = 7
tileWidth = 128
tileHeight = 64
bg.translate(w/2,tileHeight*2)
loadHashState(document.location.hash.substring(1))
drawMap()
fg = $('#fg')
fg.width = canvas.width
fg.height = canvas.height
cf = fg.getContext('2d')
cf.translate(w/2,tileHeight*2)
fg.addEventListener('mousemove', viz)
fg.addEventListener('contextmenu', e => { e.preventDefault() } )
fg.addEventListener('mouseup', unclick)
fg.addEventListener('mousedown', click)
fg.addEventListener('touchend', click)
fg.addEventListener('pointerup', click)
tools = $('#tools')
let toolCount = 0
for(let i = 0; i < texHeight; i++){
for(let j = 0; j < texWidth; j++){
const div = $c('div');
div.id = `tool_${toolCount++}`
div.style.display = "block"
/* width of 132 instead of 130 = 130 image + 2 border = 132 */
div.style.backgroundPosition = `-${j*130+2}px -${i*230}px`
div.addEventListener('click', e => {
tool = [i,j]
if (activeTool)
$(`#${activeTool}`).classList.remove('selected')
activeTool = e.target.id
$(`#${activeTool}`).classList.add('selected')
})
tools.appendChild( div )
}
}
}
// From https://stackoverflow.com/a/36046727
const ToBase64 = function(u8){
return btoa(String.fromCharCode.apply(null, u8))
}
const FromBase64 = function(str){
return atob(str).split('').map( c => c.charCodeAt(0) )
}
function updateHashState() {
let c = 0
const u8 = new Uint8Array(ntiles*ntiles)
for(let i = 0; i < ntiles; i++){
for(let j = 0; j < ntiles; j++){
u8[c++] = map[i][j][0]*texWidth + map[i][j][1]
}
}
const state = ToBase64(u8)
history.replaceState(undefined, undefined, `#${state}`)
}
function loadHashState(state) {
let u8 = FromBase64(state)
let c = 0
for(let i = 0; i < ntiles; i++) {
for(let j = 0; j < ntiles; j++) {
const t = u8[c++] || 0
const x = Math.trunc(t / texWidth)
const y = Math.trunc(t % texWidth)
map[i][j] = [x,y]
}
}
}
const click = e => {
const pos = getPosition(e)
if (pos.x >= 0 && pos.x < ntiles && pos.y >= 0 && pos.y < ntiles) {
map[pos.x][pos.y][0] = (e.which === 3) ? 0 : tool[0]
map[pos.x][pos.y][1] = (e.which === 3) ? 0 : tool[1]
isPlacing = true
drawMap()
cf.clearRect(-w, -h, w * 2, h * 2)
}
updateHashState();
}
const unclick = e => {
if (isPlacing)
isPlacing = false
}
const drawMap = function(){
bg.clearRect(-w,-h,w*2,h*2)
for(let i = 0; i < ntiles; i++){
for(let j = 0; j < ntiles; j++){
drawImageTile(bg,i,j,map[i][j][0],map[i][j][1])
}
}
}
const drawTile = function(c,x,y,color){
c.save()
c.translate((y-x) * tileWidth/2,(x+y)*tileHeight/2)
c.beginPath()
c.moveTo(0,0)
c.lineTo(tileWidth/2,tileHeight/2)
c.lineTo(0,tileHeight)
c.lineTo(-tileWidth/2,tileHeight/2)
c.closePath()
c.fillStyle = color
c.fill()
c.restore()
}
const drawImageTile = function(c,x,y,i,j){
c.save()
c.translate((y-x) * tileWidth/2,(x+y)*tileHeight/2)
j *= 130
i *= 230
c.drawImage(texture,j,i,130,230,-65,-130,130,230)
c.restore()
}
const getPosition = e => {
const _y = (e.offsetY - tileHeight * 2) / tileHeight,
_x = e.offsetX / tileWidth - ntiles / 2
x = Math.floor(_y-_x)
y = Math.floor(_x+_y)
return {x,y}
}
const viz = function(e){
if (isPlacing)
click(e)
const pos = getPosition(e)
cf.clearRect(-w,-h,w*2,h*2)
if( pos.x >= 0 && pos.x < ntiles && pos.y >= 0 && pos.y < ntiles)
drawTile(cf,pos.x,pos.y,'rgba(0,0,0,0.2)')
}
html, body {
margin: 0;
padding: 0;
height: 100%;
}
canvas {
display: block;
position: absolute;
}
#main {
position: relative;
display: flex;
align-items: center;
justify-content: space-around;
height: 100%;
}
#area {
position: relative;
width: 100%;
height: 100%;
flex: 1;
display: flex;
justify-content: space-around;
overflow: auto;
}
#tools {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: wrap;
overflow: auto;
width: 580px;
height: 100%;
transition: width .8s;
}
@media only screen and (max-width: 1700px) {
#tools {
width: 440px;
}
}
@media only screen and (max-width: 1540px) {
#tools {
width: 300px;
}
}
@media only screen and (max-width: 1380px) {
#tools {
width: 160px;
}
}
#tools > div {
display: block;
background-image: url('../textures/01_130x66_130x230.png');
background-repeat: no-repeat;
background-size: auto;
width: 130px;
height: 230px;
border: 2px dashed transparent;
box-sizing: border-box;
}
#tools > div.selected {
border-color: #b05355;
}
@media only screen and (max-width: 966px) {
#main {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
}
#tools {
display: flex;
align-items: center;
overflow: auto;
width: 100%;
height: 240px;
}
#area {
justify-content: flex-start;
}
}