2020-11-08 14:13:43 +01:00
|
|
|
import sizeOf from 'image-size'
|
2020-12-22 22:35:09 +01:00
|
|
|
import Util from '../common/Util.js'
|
2020-12-09 00:15:04 +01:00
|
|
|
import exif from 'exif'
|
2020-12-21 18:34:57 +01:00
|
|
|
import { Rng } from '../common/Rng.js'
|
2020-11-08 14:13:43 +01:00
|
|
|
|
|
|
|
|
// cut size of each puzzle tile in the
|
|
|
|
|
// final resized version of the puzzle image
|
|
|
|
|
const TILE_SIZE = 64
|
|
|
|
|
|
2020-12-09 00:15:04 +01:00
|
|
|
async function getDimensions(imagePath) {
|
|
|
|
|
let dimensions = sizeOf(imagePath)
|
|
|
|
|
try {
|
|
|
|
|
const orientation = await getExifOrientation(imagePath)
|
|
|
|
|
// when image is rotated to the left or right, switch width/height
|
|
|
|
|
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
|
|
|
|
|
if (orientation === 6 || orientation === 8) {
|
|
|
|
|
return {
|
|
|
|
|
width: dimensions.height,
|
|
|
|
|
height: dimensions.width,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
return dimensions
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getExifOrientation(imagePath) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
new exif.ExifImage({ image: imagePath }, function (error, exifData) {
|
|
|
|
|
if (error) {
|
|
|
|
|
reject(error)
|
|
|
|
|
} else {
|
|
|
|
|
resolve(exifData.image.Orientation)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-21 18:34:57 +01:00
|
|
|
async function createPuzzle(
|
|
|
|
|
/** @type Rng */ rng,
|
|
|
|
|
targetTiles,
|
2020-12-22 22:35:09 +01:00
|
|
|
image,
|
|
|
|
|
ts
|
2020-12-21 18:34:57 +01:00
|
|
|
) {
|
2020-11-25 22:03:35 +01:00
|
|
|
const imagePath = image.file
|
|
|
|
|
const imageUrl = image.url
|
2020-11-08 14:49:34 +01:00
|
|
|
|
2020-11-08 14:13:43 +01:00
|
|
|
// load bitmap, to determine the original size of the image
|
2020-12-09 00:15:04 +01:00
|
|
|
const dim = await getDimensions(imagePath)
|
2020-11-08 14:13:43 +01:00
|
|
|
|
|
|
|
|
// determine puzzle information from the bitmap
|
2020-11-16 21:43:49 +01:00
|
|
|
const info = determinePuzzleInfo(dim.width, dim.height, targetTiles)
|
2020-11-08 14:13:43 +01:00
|
|
|
|
|
|
|
|
let tiles = new Array(info.tiles)
|
|
|
|
|
for (let i = 0; i < tiles.length; i++) {
|
2020-11-16 21:43:49 +01:00
|
|
|
tiles[i] = { idx: i }
|
2020-11-08 14:13:43 +01:00
|
|
|
}
|
2020-12-21 18:34:57 +01:00
|
|
|
const shapes = determinePuzzleTileShapes(rng, info)
|
2020-11-08 14:13:43 +01:00
|
|
|
|
2020-11-08 16:42:59 +01:00
|
|
|
let positions = new Array(info.tiles)
|
|
|
|
|
for (let tile of tiles) {
|
2020-12-05 19:45:34 +01:00
|
|
|
let coord = Util.coordByTileIdx(info, tile.idx)
|
2020-11-08 16:42:59 +01:00
|
|
|
positions[tile.idx] ={
|
|
|
|
|
// instead of info.tileSize, we use info.tileDrawSize
|
|
|
|
|
// to spread the tiles a bit
|
2020-12-05 19:45:34 +01:00
|
|
|
x: coord.x * info.tileSize * 1.5,
|
|
|
|
|
y: coord.y * info.tileSize * 1.5,
|
2020-11-08 16:42:59 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-16 21:43:49 +01:00
|
|
|
const tableWidth = info.width * 3
|
|
|
|
|
const tableHeight = info.height * 3
|
2020-11-08 16:42:59 +01:00
|
|
|
|
2020-11-16 21:43:49 +01:00
|
|
|
const off = info.tileSize * 1.5
|
|
|
|
|
let last = {
|
|
|
|
|
x: info.width - (1 * off),
|
|
|
|
|
y: info.height - (2 * off),
|
|
|
|
|
}
|
|
|
|
|
let countX = Math.ceil(info.width / off) + 2
|
|
|
|
|
let countY = Math.ceil(info.height / off) + 2
|
2020-11-08 16:42:59 +01:00
|
|
|
|
2020-11-16 21:43:49 +01:00
|
|
|
let diffX = off
|
|
|
|
|
let diffY = 0
|
2020-11-08 16:42:59 +01:00
|
|
|
let index = 0
|
|
|
|
|
for (let pos of positions) {
|
|
|
|
|
pos.x = last.x
|
|
|
|
|
pos.y = last.y
|
2020-11-16 21:43:49 +01:00
|
|
|
last.x+=diffX
|
|
|
|
|
last.y+=diffY
|
2020-11-08 16:42:59 +01:00
|
|
|
index++
|
|
|
|
|
// did we move horizontally?
|
2020-11-16 21:43:49 +01:00
|
|
|
if (diffX !== 0) {
|
|
|
|
|
if (index === countX) {
|
|
|
|
|
diffY = diffX
|
|
|
|
|
countY++
|
|
|
|
|
diffX = 0
|
2020-11-08 16:42:59 +01:00
|
|
|
index = 0
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2020-11-16 21:43:49 +01:00
|
|
|
if (index === countY) {
|
|
|
|
|
diffX = -diffY
|
|
|
|
|
countX++
|
|
|
|
|
diffY = 0
|
2020-11-08 16:42:59 +01:00
|
|
|
index = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// then shuffle the positions
|
2020-12-21 18:34:57 +01:00
|
|
|
positions = Util.shuffle(rng, positions)
|
2020-11-08 16:42:59 +01:00
|
|
|
|
|
|
|
|
tiles = tiles.map(tile => {
|
2020-12-05 19:45:34 +01:00
|
|
|
return Util.encodeTile({
|
2020-11-08 16:42:59 +01:00
|
|
|
idx: tile.idx, // index of tile in the array
|
|
|
|
|
group: 0, // if grouped with other tiles
|
|
|
|
|
z: 0, // z index of the tile
|
|
|
|
|
|
|
|
|
|
// who owns the tile
|
|
|
|
|
// 0 = free for taking
|
|
|
|
|
// -1 = finished
|
|
|
|
|
// other values: id of player who has the tile
|
|
|
|
|
owner: 0,
|
|
|
|
|
|
|
|
|
|
// physical current position of the tile (x/y in pixels)
|
|
|
|
|
// this position is the initial position only and is the
|
|
|
|
|
// value that changes when moving a tile
|
|
|
|
|
pos: positions[tile.idx],
|
2020-12-05 19:45:34 +01:00
|
|
|
})
|
2020-11-08 16:42:59 +01:00
|
|
|
})
|
|
|
|
|
|
2020-11-08 14:13:43 +01:00
|
|
|
// Complete puzzle object
|
2020-11-16 21:43:49 +01:00
|
|
|
return {
|
2020-11-08 14:13:43 +01:00
|
|
|
// tiles array
|
2020-11-08 16:42:59 +01:00
|
|
|
tiles,
|
2020-11-08 14:13:43 +01:00
|
|
|
// game data for puzzle, data changes during the game
|
|
|
|
|
data: {
|
|
|
|
|
// TODO: maybe calculate this each time?
|
|
|
|
|
maxZ: 0, // max z of all pieces
|
|
|
|
|
maxGroup: 0, // max group of all pieces
|
2020-12-22 22:35:09 +01:00
|
|
|
started: ts, // start timestamp
|
2020-12-06 21:55:23 +01:00
|
|
|
finished: 0, // finish timestamp
|
2020-11-08 14:13:43 +01:00
|
|
|
},
|
|
|
|
|
// static puzzle information. stays same for complete duration of
|
|
|
|
|
// the game
|
|
|
|
|
info: {
|
2020-11-08 16:42:59 +01:00
|
|
|
table: {
|
|
|
|
|
width: tableWidth,
|
|
|
|
|
height: tableHeight,
|
|
|
|
|
},
|
2020-11-08 14:13:43 +01:00
|
|
|
// information that was used to create the puzzle
|
|
|
|
|
targetTiles: targetTiles,
|
2020-11-16 21:43:49 +01:00
|
|
|
imageUrl,
|
2020-11-08 14:13:43 +01:00
|
|
|
|
|
|
|
|
width: info.width, // actual puzzle width (same as bitmap.width)
|
|
|
|
|
height: info.height, // actual puzzle height (same as bitmap.height)
|
|
|
|
|
tileSize: info.tileSize, // width/height of each tile (without tabs)
|
|
|
|
|
tileDrawSize: info.tileDrawSize, // width/height of each tile (with tabs)
|
|
|
|
|
tileMarginWidth: info.tileMarginWidth,
|
|
|
|
|
// offset in x and y when drawing tiles, so that they appear to be at pos
|
|
|
|
|
tileDrawOffset: (info.tileDrawSize - info.tileSize) / -2,
|
|
|
|
|
// max distance between tile and destination that
|
|
|
|
|
// makes the tile snap to destination
|
|
|
|
|
snapDistance: info.tileSize / 2,
|
|
|
|
|
tiles: info.tiles, // the final number of tiles in the puzzle
|
2020-11-16 21:43:49 +01:00
|
|
|
tilesX: info.tilesX, // number of tiles each row
|
|
|
|
|
tilesY: info.tilesY, // number of tiles each col
|
2020-11-08 14:13:43 +01:00
|
|
|
// ( index => {x, y} )
|
|
|
|
|
// this is not the physical coordinate, but
|
|
|
|
|
// the tile_coordinate
|
|
|
|
|
// this can be used to determine where the
|
|
|
|
|
// final destination of a tile is
|
|
|
|
|
shapes: shapes, // tile shapes
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-21 18:34:57 +01:00
|
|
|
function determinePuzzleTileShapes(
|
|
|
|
|
/** @type Rng */ rng,
|
|
|
|
|
info
|
|
|
|
|
) {
|
2020-11-08 14:13:43 +01:00
|
|
|
const tabs = [-1, 1]
|
|
|
|
|
|
|
|
|
|
const shapes = new Array(info.tiles)
|
|
|
|
|
for (let i = 0; i < info.tiles; i++) {
|
2020-12-05 19:45:34 +01:00
|
|
|
let coord = Util.coordByTileIdx(info, i)
|
2020-12-06 00:14:55 +01:00
|
|
|
shapes[i] = {
|
2020-12-05 19:45:34 +01:00
|
|
|
top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1,
|
2020-12-21 18:34:57 +01:00
|
|
|
right: coord.x === info.tilesX - 1 ? 0 : Util.choice(rng, tabs),
|
2020-12-05 19:45:34 +01:00
|
|
|
left: coord.x === 0 ? 0 : shapes[i - 1].right * -1,
|
2020-12-21 18:34:57 +01:00
|
|
|
bottom: coord.y === info.tilesY - 1 ? 0 : Util.choice(rng, tabs),
|
2020-12-06 00:14:55 +01:00
|
|
|
}
|
2020-11-08 14:13:43 +01:00
|
|
|
}
|
2020-12-06 00:14:55 +01:00
|
|
|
return shapes.map(Util.encodeShape)
|
2020-11-08 14:13:43 +01:00
|
|
|
}
|
|
|
|
|
|
2020-11-16 21:43:49 +01:00
|
|
|
const determineTilesXY = (w, h, targetTiles) => {
|
|
|
|
|
const w_ = w < h ? (w * h) : (w * w)
|
|
|
|
|
const h_ = w < h ? (h * h) : (w * h)
|
|
|
|
|
let size = 0
|
2020-11-08 14:13:43 +01:00
|
|
|
let tiles = 0
|
|
|
|
|
do {
|
2020-11-16 21:43:49 +01:00
|
|
|
size++
|
|
|
|
|
tiles = Math.floor(w_ / size) * Math.floor(h_ / size)
|
2020-11-08 14:13:43 +01:00
|
|
|
} while (tiles >= targetTiles)
|
2020-11-16 21:43:49 +01:00
|
|
|
size--
|
|
|
|
|
return {
|
|
|
|
|
tilesX: Math.round(w_ / size),
|
|
|
|
|
tilesY: Math.round(h_ / size),
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-11-08 14:13:43 +01:00
|
|
|
|
2020-11-16 21:43:49 +01:00
|
|
|
const determinePuzzleInfo = (w, h, targetTiles) => {
|
|
|
|
|
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
|
|
|
|
|
const tiles = tilesX * tilesY
|
|
|
|
|
const tileSize = TILE_SIZE
|
|
|
|
|
const width = tilesX * tileSize
|
|
|
|
|
const height = tilesY * tileSize
|
2020-11-08 14:13:43 +01:00
|
|
|
|
|
|
|
|
const tileMarginWidth = tileSize * .5;
|
|
|
|
|
const tileDrawSize = Math.round(tileSize + tileMarginWidth * 2)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
tileSize,
|
|
|
|
|
tileMarginWidth,
|
|
|
|
|
tileDrawSize,
|
|
|
|
|
tiles,
|
2020-11-16 21:43:49 +01:00
|
|
|
tilesX,
|
|
|
|
|
tilesY,
|
2020-11-08 14:13:43 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export {
|
2020-11-12 19:19:02 +01:00
|
|
|
createPuzzle,
|
2020-11-08 14:13:43 +01:00
|
|
|
}
|