puzzle/src/frontend/PuzzleGraphics.ts

229 lines
8 KiB
TypeScript
Raw Normal View History

2021-05-09 13:49:40 +02:00
"use strict"
2021-05-17 02:32:33 +02:00
import Geometry, { Rect } from '../common/Geometry'
2021-05-17 00:27:47 +02:00
import Graphics from './Graphics'
import Util, { logger } from './../common/Util'
import { Puzzle, PuzzleInfo, PieceShape, EncodedPiece } from './../common/Types'
const log = logger('PuzzleGraphics.js')
2020-11-17 21:46:56 +01:00
2021-05-17 02:32:33 +02:00
async function createPuzzleTileBitmaps(
img: ImageBitmap,
2021-05-29 17:58:05 +02:00
pieces: EncodedPiece[],
2021-05-17 02:32:33 +02:00
info: PuzzleInfo
): Promise<Array<ImageBitmap>> {
log.log('start createPuzzleTileBitmaps')
2021-05-29 17:58:05 +02:00
const tileSize = info.tileSize
const tileMarginWidth = info.tileMarginWidth
const tileDrawSize = info.tileDrawSize
const tileRatio = tileSize / 100.0
2020-11-17 21:46:56 +01:00
2021-05-29 17:58:05 +02:00
const curvyCoords = [
2020-11-17 21:46:56 +01:00
0, 0, 40, 15, 37, 5,
37, 5, 40, 0, 38, -5,
38, -5, 20, -20, 50, -20,
50, -20, 80, -20, 62, -5,
62, -5, 60, 0, 63, 5,
63, 5, 65, 15, 100, 0
];
2021-05-29 17:58:05 +02:00
const bitmaps: Array<ImageBitmap> = new Array(pieces.length)
2020-11-17 21:46:56 +01:00
2021-05-17 00:27:47 +02:00
const paths: Record<string, Path2D> = {}
function pathForShape(shape: PieceShape) {
2020-11-17 21:46:56 +01:00
const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}`
if (paths[key]) {
return paths[key]
}
const path = new Path2D()
const topLeftEdge = { x: tileMarginWidth, y: tileMarginWidth }
2020-11-25 22:03:35 +01:00
const topRightEdge = Geometry.pointAdd(topLeftEdge, { x: tileSize, y: 0 })
const bottomRightEdge = Geometry.pointAdd(topRightEdge, { x: 0, y: tileSize })
const bottomLeftEdge = Geometry.pointSub(bottomRightEdge, { x: tileSize, y: 0 })
2020-11-17 21:46:56 +01:00
path.moveTo(topLeftEdge.x, topLeftEdge.y)
2020-11-25 22:03:35 +01:00
if (shape.top !== 0) {
for (let i = 0; i < curvyCoords.length / 6; i++) {
const p1 = Geometry.pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.top * curvyCoords[i * 6 + 1] * tileRatio })
const p2 = Geometry.pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.top * curvyCoords[i * 6 + 3] * tileRatio })
const p3 = Geometry.pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.top * curvyCoords[i * 6 + 5] * tileRatio })
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
} else {
path.lineTo(topRightEdge.x, topRightEdge.y)
2020-11-17 21:46:56 +01:00
}
2020-11-25 22:03:35 +01:00
if (shape.right !== 0) {
for (let i = 0; i < curvyCoords.length / 6; i++) {
const p1 = Geometry.pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
const p2 = Geometry.pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
const p3 = Geometry.pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio })
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
} else {
path.lineTo(bottomRightEdge.x, bottomRightEdge.y)
2020-11-17 21:46:56 +01:00
}
2020-11-25 22:03:35 +01:00
if (shape.bottom !== 0) {
for (let i = 0; i < curvyCoords.length / 6; i++) {
2021-05-29 17:58:05 +02:00
const p1 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio })
const p2 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * tileRatio })
const p3 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 5] * tileRatio })
2020-11-25 22:03:35 +01:00
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
} else {
path.lineTo(bottomLeftEdge.x, bottomLeftEdge.y)
2020-11-17 21:46:56 +01:00
}
2020-11-25 22:03:35 +01:00
if (shape.left !== 0) {
for (let i = 0; i < curvyCoords.length / 6; i++) {
2021-05-29 17:58:05 +02:00
const p1 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
const p2 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
const p3 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio })
2020-11-25 22:03:35 +01:00
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
} else {
path.lineTo(topLeftEdge.x, topLeftEdge.y)
2020-11-17 21:46:56 +01:00
}
paths[key] = path
return path
}
const c = Graphics.createCanvas(tileDrawSize, tileDrawSize)
2021-05-17 00:27:47 +02:00
const ctx = c.getContext('2d') as CanvasRenderingContext2D
const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize)
2021-05-17 00:27:47 +02:00
const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D
2021-05-29 17:58:05 +02:00
for (const p of pieces) {
const piece = Util.decodePiece(p)
const srcRect = srcRectByIdx(info, piece.idx)
const path = pathForShape(Util.decodeShape(info.shapes[piece.idx]))
2020-11-17 21:46:56 +01:00
ctx.clearRect(0, 0, tileDrawSize, tileDrawSize)
2020-11-25 22:03:35 +01:00
// stroke (slightly darker version of image)
2020-11-17 21:46:56 +01:00
// -----------------------------------------------------------
// -----------------------------------------------------------
2020-11-25 22:03:35 +01:00
ctx.save()
2020-11-17 21:46:56 +01:00
ctx.lineWidth = 2
ctx.stroke(path)
2020-11-25 22:03:35 +01:00
ctx.globalCompositeOperation = 'source-in'
ctx.drawImage(
img,
srcRect.x - tileMarginWidth,
srcRect.y - tileMarginWidth,
tileDrawSize,
tileDrawSize,
0,
0,
tileDrawSize,
tileDrawSize,
)
ctx.restore()
ctx.save()
ctx.globalCompositeOperation = 'source-in'
ctx.globalAlpha = .2
ctx.fillStyle = 'black'
ctx.fillRect(0,0, c.width, c.height)
ctx.restore()
// main image
2020-11-17 21:46:56 +01:00
// -----------------------------------------------------------
// -----------------------------------------------------------
2020-11-25 22:03:35 +01:00
ctx.save()
2020-11-17 21:46:56 +01:00
ctx.clip(path)
ctx.drawImage(
img,
srcRect.x - tileMarginWidth,
srcRect.y - tileMarginWidth,
tileDrawSize,
tileDrawSize,
0,
0,
tileDrawSize,
tileDrawSize,
)
2020-11-25 22:03:35 +01:00
ctx.restore()
// INSET SHADOW (bottom, right)
// -----------------------------------------------------------
// -----------------------------------------------------------
ctx.save()
ctx.clip(path)
ctx.strokeStyle = 'rgba(0,0,0,.4)'
ctx.lineWidth = 0
ctx.shadowColor = "black";
ctx.shadowBlur = 2;
ctx.shadowOffsetX = -1;
ctx.shadowOffsetY = -1;
2020-11-17 21:46:56 +01:00
ctx.stroke(path)
2020-11-25 22:03:35 +01:00
ctx.restore()
// INSET SHADOW (top, left)
// -----------------------------------------------------------
// -----------------------------------------------------------
ctx.save()
ctx.clip(path)
ctx.strokeStyle = 'rgba(255,255,255,.4)'
ctx.lineWidth = 0
ctx.shadowColor = "white";
ctx.shadowBlur = 2;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.stroke(path)
ctx.restore()
2021-04-17 17:50:03 +02:00
// Redraw the path (border) in the color of the
// tile, this makes the tile look more realistic
2020-11-25 22:03:35 +01:00
// -----------------------------------------------------------
// -----------------------------------------------------------
ctx2.clearRect(0, 0, tileDrawSize, tileDrawSize)
2020-11-25 22:03:35 +01:00
ctx2.save()
ctx2.lineWidth = 1
ctx2.stroke(path)
ctx2.globalCompositeOperation = 'source-in'
ctx2.drawImage(
img,
2021-04-17 17:50:03 +02:00
srcRect.x - tileMarginWidth,
srcRect.y - tileMarginWidth,
tileDrawSize,
tileDrawSize,
0,
0,
tileDrawSize,
tileDrawSize,
2020-11-25 22:03:35 +01:00
)
ctx2.restore()
ctx.drawImage(c2, 0, 0)
2020-11-17 21:46:56 +01:00
2021-05-29 17:58:05 +02:00
bitmaps[piece.idx] = await createImageBitmap(c)
2020-11-17 21:46:56 +01:00
}
log.log('end createPuzzleTileBitmaps')
2020-11-17 21:46:56 +01:00
return bitmaps
}
2021-05-17 02:32:33 +02:00
function srcRectByIdx(puzzleInfo: PuzzleInfo, idx: number): Rect {
2021-05-29 17:58:05 +02:00
const c = Util.coordByPieceIdx(puzzleInfo, idx)
2020-11-17 21:46:56 +01:00
return {
x: c.x * puzzleInfo.tileSize,
y: c.y * puzzleInfo.tileSize,
w: puzzleInfo.tileSize,
h: puzzleInfo.tileSize,
}
}
2021-05-17 02:32:33 +02:00
async function loadPuzzleBitmaps(puzzle: Puzzle): Promise<Array<ImageBitmap>> {
2020-11-17 21:46:56 +01:00
// load bitmap, to determine the original size of the image
const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)
// creation of tile bitmaps
// then create the final puzzle bitmap
// NOTE: this can decrease OR increase in size!
const bmpResized = await Graphics.resizeBitmap(bmp, puzzle.info.width, puzzle.info.height)
return await createPuzzleTileBitmaps(bmpResized, puzzle.tiles, puzzle.info)
}
export default {
loadPuzzleBitmaps,
}