"use strict" import Geometry, { Rect } from '../common/Geometry' import Graphics from './Graphics' import Util, { logger } from './../common/Util' import { Puzzle, PuzzleInfo, PieceShape, EncodedPiece } from './../common/GameCommon' const log = logger('PuzzleGraphics.js') async function createPuzzleTileBitmaps( img: ImageBitmap, pieces: EncodedPiece[], info: PuzzleInfo ): Promise> { log.log('start createPuzzleTileBitmaps') const tileSize = info.tileSize const tileMarginWidth = info.tileMarginWidth const tileDrawSize = info.tileDrawSize const tileRatio = tileSize / 100.0 const curvyCoords = [ 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 ]; const bitmaps: Array = new Array(pieces.length) const paths: Record = {} function pathForShape(shape: PieceShape) { 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 } 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 }) path.moveTo(topLeftEdge.x, topLeftEdge.y) 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) } 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) } if (shape.bottom !== 0) { for (let i = 0; i < curvyCoords.length / 6; i++) { 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 }) path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); } } else { path.lineTo(bottomLeftEdge.x, bottomLeftEdge.y) } if (shape.left !== 0) { for (let i = 0; i < curvyCoords.length / 6; i++) { 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 }) path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); } } else { path.lineTo(topLeftEdge.x, topLeftEdge.y) } paths[key] = path return path } const c = Graphics.createCanvas(tileDrawSize, tileDrawSize) const ctx = c.getContext('2d') as CanvasRenderingContext2D const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize) const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D 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])) ctx.clearRect(0, 0, tileDrawSize, tileDrawSize) // stroke (slightly darker version of image) // ----------------------------------------------------------- // ----------------------------------------------------------- ctx.save() ctx.lineWidth = 2 ctx.stroke(path) 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 // ----------------------------------------------------------- // ----------------------------------------------------------- ctx.save() ctx.clip(path) ctx.drawImage( img, srcRect.x - tileMarginWidth, srcRect.y - tileMarginWidth, tileDrawSize, tileDrawSize, 0, 0, tileDrawSize, tileDrawSize, ) 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; ctx.stroke(path) 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() // Redraw the path (border) in the color of the // tile, this makes the tile look more realistic // ----------------------------------------------------------- // ----------------------------------------------------------- ctx2.clearRect(0, 0, tileDrawSize, tileDrawSize) ctx2.save() ctx2.lineWidth = 1 ctx2.stroke(path) ctx2.globalCompositeOperation = 'source-in' ctx2.drawImage( img, srcRect.x - tileMarginWidth, srcRect.y - tileMarginWidth, tileDrawSize, tileDrawSize, 0, 0, tileDrawSize, tileDrawSize, ) ctx2.restore() ctx.drawImage(c2, 0, 0) bitmaps[piece.idx] = await createImageBitmap(c) } log.log('end createPuzzleTileBitmaps') return bitmaps } function srcRectByIdx(puzzleInfo: PuzzleInfo, idx: number): Rect { const c = Util.coordByPieceIdx(puzzleInfo, idx) return { x: c.x * puzzleInfo.tileSize, y: c.y * puzzleInfo.tileSize, w: puzzleInfo.tileSize, h: puzzleInfo.tileSize, } } async function loadPuzzleBitmaps(puzzle: Puzzle): Promise> { // 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, }