blob: 5f317cb96e6fa59f7954634a6219e8c135f8d1eb [file] [log] [blame]
<!DOCTYPE html>
<meta charset=utf-8>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { text-align: center; white-space: nowrap; font: 16px Helvetica, sans-serif; }
img, #puzzleboard { box-shadow: 0 0 4px black; }
img { margin: 32px 8px 0; }
#puzzleboard { display: inline-flex; flex-direction: column; background-color: tan; padding: 16px; border-radius: 8px; }
canvas { border: 2px solid; border-color: rgba(0, 0, 0, 0.2) rgba(255, 255, 255, 0.2) rgba(255, 255, 255, 0.2) rgba(0, 0, 0, 0.2); background-color: rgba(255, 255, 255, 0.2); padding: 2px; }
label { padding-top: 16px; font-weight: bold; color: #864; text-shadow: 0px 2px 0px rgba(255, 255, 255, 0.3); color: rgb(104, 90, 70); border-radius: 8px; -webkit-user-select: none; user-select: none; text-align: center; }
input { vertical-align: middle; }
label span { display: none; }
.warning { display: none; }
.unsupported .warning { display: block; position: absolute; top: 0; left: 0; right: 0; }
.warning span { background-color: rgba(0, 0, 0, 0.5); padding: 4px; border; border-radius: 4px; color: white; }
.unsupported label { text-decoration: line-through; }
@media (prefers-color-scheme: dark) {
html { background-color: black; }
img, #puzzleboard { box-shadow: 0 0 4px #fff8; }
}
@media (min-width: 500px) {
label span { display: inline; }
img { margin-right: 16px; }
}
@media (min-width: 1000px) {
img { margin-right: 32px; }
}
</style>
<script>
const SIZE = 3;
const EMPTY_SQUARE = SIZE * SIZE - 1;
const TILE_CORNER_RADIUS = 8;
let supported = false;
let width, height, board, blank;
let canvas, ctx;
let scale = 1;
let animationX, animationY, animatedPosition, animationProgress, animationStartTime, animationActive;
let complete = false;
let cachedCanvases = {};
matchMedia("(min-width: 500px)").onchange = relayout;
matchMedia("(min-width: 1000px)").onchange = relayout;
function generateGrain() {
let w = puzzleboard.offsetWidth;
let h = puzzleboard.offsetHeight;
let d = "";
let y = 0;
for (;;) {
y += 5 + Math.round(Math.random() * 5);
if (y >= h)
break;
let x = 0 + Math.round(Math.random() * w / 8);
let length = Math.round((3 + Math.random()) / 4 * w);
d += `M${x},${y}h${length}`;
}
let svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
<defs>
<path id="p" d="${d}"/>
</defs>
<g opacity="0.1">
<use href="#p" stroke="black"/>
<use href="#p" stroke="white" transform="translate(0,1)"/>
</g>
</svg>`;
return "data:image/svg+xml;base64," + btoa(svg);
}
function canvasSupportsDisplayP3() {
let canvas = document.createElement("canvas");
try {
let context = canvas.getContext("2d", { colorSpace: "display-p3" });
return context.getContextAttributes().colorSpace == "display-p3";
} catch {
}
return false;
}
function init() {
supported = canvasSupportsDisplayP3();
if (!supported)
document.body.className = "unsupported";
canvas = placeholder;
width = image.naturalWidth;
height = image.naturalHeight;
board = [];
for (let position = 0; position < SIZE * SIZE; ++position)
board.push(position);
puzzleboard.style.backgroundImage = `url(${generateGrain()})`;
relayout();
blank = SIZE * SIZE - 1;
shuffle();
drawBoard();
}
function relayout() {
if (!canvas)
return;
if (window.innerWidth < 500)
scale = 3;
else if (window.innerWidth < 1000)
scale = 2;
else
scale = 1;
image.width = width / scale;
image.height = height / scale;
updateCanvas();
}
function shuffle() {
let lastBlank = blank;
for (let i = 0; i < 20; ++i) {
let validMoves = [];
if (blank % SIZE != 0 && lastBlank != blank - 1)
validMoves.push(blank - 1);
if (blank % SIZE != SIZE - 1 && lastBlank != blank + 1)
validMoves.push(blank + 1);
if (Math.floor(blank / SIZE) != 0 && lastBlank != blank - SIZE)
validMoves.push(blank - SIZE);
if (Math.floor(blank / SIZE) != SIZE - 1 && lastBlank != blank + SIZE)
validMoves.push(blank + SIZE);
let move = validMoves[Math.floor(Math.random() * validMoves.length)];
lastBlank = blank;
makeMove(move);
}
}
function makeMove(position) {
board[blank] = board[position];
board[position] = EMPTY_SQUARE;
blank = position;
}
function drawTile(position) {
let segment = board[position];
if (segment == EMPTY_SQUARE)
return;
let x = position % SIZE;
let y = Math.floor(position / SIZE);
let tileWidth = width / SIZE;
let tileHeight = height / SIZE;
let radius = TILE_CORNER_RADIUS * scale;
let tilePath = new Path2D(`
M ${2 + radius},2
h ${tileWidth - 4 - radius * 2} a ${radius},${radius} 0 0 1 ${radius}, ${radius}
v ${tileWidth - 4 - radius * 2} a ${radius},${radius} 0 0 1 -${radius}, ${radius}
h -${tileWidth - 4 - radius * 2} a ${radius},${radius} 0 0 1 -${radius},-${radius}
v -${tileWidth - 4 - radius * 2} a ${radius},${radius} 0 0 1 ${radius},-${radius}
z`);
ctx.save();
ctx.translate(x * tileWidth, y * tileHeight);
if (animationActive && position == animatedPosition)
ctx.translate(animationX * animationProgress, animationY * animationProgress);
ctx.clip(tilePath);
ctx.drawImage(image, (segment % SIZE) * tileWidth, Math.floor(segment / SIZE) * tileHeight, tileWidth, tileHeight, 0, 0, tileWidth, tileHeight);
ctx.restore();
}
function drawBoard() {
ctx.clearRect(0, 0, width, height);
if (complete) {
ctx.drawImage(image, 0, 0, width, height);
} else {
for (let position = 0; position < SIZE * SIZE; ++position)
drawTile(position);
}
}
function updateCanvas() {
let colorSpace = displayP3.checked && supported ? "display-p3" : "srgb";
let newCanvas = cachedCanvases[colorSpace];
if (newCanvas != canvas) {
if (!newCanvas) {
newCanvas = document.createElement("canvas");
newCanvas.width = image.naturalWidth;
newCanvas.height = image.naturalHeight;
newCanvas.addEventListener("mousedown", mouseDown, false);
cachedCanvases[colorSpace] = newCanvas;
}
}
canvas.replaceWith(newCanvas);
canvas = newCanvas;
canvas.style.width = `${width / scale}px`;
canvas.style.height = `${height / scale}px`;
ctx = canvas.getContext("2d", { colorSpace });
drawBoard();
}
function mouseDown(event) {
if (animationActive || complete)
return;
let tileWidth = width / SIZE;
let tileHeight = height / SIZE;
let x = event.offsetX * scale;
let y = event.offsetY * scale;
let position = Math.floor((x - 8) / tileWidth) + Math.floor((y - 8) / tileHeight) * SIZE;
if (blank == position - SIZE) {
animationX = 0;
animationY = -tileHeight;
} else if (blank == position + SIZE) {
animationX = 0;
animationY = tileHeight;
} else if (blank == position - 1 && (position % SIZE != 0)) {
animationX = -tileWidth;
animationY = 0;
} else if (blank == position + 1 && (position % SIZE != SIZE - 1)) {
animationX = tileWidth;
animationY = 0;
} else
return;
animatedPosition = position;
animationStartTime = Date.now();
animationActive = true;
requestAnimationFrame(animate);
}
function isComplete() {
for (let position = 0; position < SIZE * SIZE; ++position) {
if (board[position] != position)
return false;
}
return true;
}
function animate() {
animationProgress = (Date.now() - animationStartTime) / 100;
if (animationProgress < 1) {
drawBoard();
requestAnimationFrame(animate);
return;
}
animationActive = false;
makeMove(animatedPosition);
if (isComplete()) {
complete = true;
}
drawBoard();
}
</script>
<img id="image" src="puzzle.jpg" onload="init()">
<div id=puzzleboard>
<div id="placeholder"></div>
<label><input id=displayP3 type=checkbox onchange="updateCanvas()"> Use Display P3 <span>canvas</span></label>
</div>
<div class="warning"><span>This browser does not support Display P3 canvas.</span></div>