Skip to content

Instantly share code, notes, and snippets.

@aldoyh
Last active June 23, 2026 02:49
Show Gist options
  • Select an option

  • Save aldoyh/0b54033d05c8fdf9022d94825d92ac74 to your computer and use it in GitHub Desktop.

Select an option

Save aldoyh/0b54033d05c8fdf9022d94825d92ac74 to your computer and use it in GitHub Desktop.
Halloween 3D Jigsaw Puzzle

Halloween 3D Jigsaw Puzzle

Playing with Threejs and Jigsaw.

This is one among any tests I did, and still miss lots of improvement, example pieces collision but I couldnt find a quick solution to combine both the solid collision and the board static positioning.

A Pen by Den on CodePen.

License.

@property --halftone-intensity {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
<div id="preview-container">
<img src="https://assets.codepen.io/453571/halloween-pumpkins-smiling.avif" id="preview-image" alt="Puzzle Preview" crossorigin />
</div>
<input type="checkbox" name="view-reference" id="view-reference">
<label for="view-reference" class="cheat_reveal" tabindex="1">
<div class="view-reference_txt">Reveal the reference</div>
</label>
<!-- filter -->
<svg class="svg-half-tone-dots" x="0" y="0" width="0" height="0" focusable="false" aria-hidden="hidden" inert="" style="position:absolute; pointer-events:none;">
<defs>
<style>
:has(:where(img:hover, #view-reference:checked)) {
--reveal: black
}
</style>
<!-- Circles from r=0.25 to r=2 -->
<circle id="dot1" cx="2" cy="2" r="0.25"></circle>
<circle id="dot2" cx="2" cy="2" r="0.5"></circle>
<circle id="dot3" cx="2" cy="2" r="0.75"></circle>
<circle id="dot4" cx="2" cy="2" r="1"></circle>
<circle id="dot5" cx="2" cy="2" r="1.25"></circle>
<circle id="dot6" cx="2" cy="2" r="1.5"></circle>
<circle id="dot7" cx="2" cy="2" r="1.75"></circle>
<circle id="dot8" cx="2" cy="2" r="2"></circle>
<filter id="half-tone-filter" color-interpolation-filters="sRGB" primitiveUnits="userSpaceOnUse">
<!-- Tiled circles -->
<feImage width="4" height="4" xlink:href="#dot1"></feImage>
<feTile result="dot1-tile"></feTile>
<feImage width="4" height="4" xlink:href="#dot2"></feImage>
<feTile result="dot2-tile"></feTile>
<feImage width="4" height="4" xlink:href="#dot3"></feImage>
<feTile result="dot3-tile"></feTile>
<feImage width="4" height="4" xlink:href="#dot4"></feImage>
<feTile result="dot4-tile"></feTile>
<feImage width="4" height="4" xlink:href="#dot5"></feImage>
<feTile result="dot5-tile"></feTile>
<feImage width="4" height="4" xlink:href="#dot6"></feImage>
<feTile result="dot6-tile"></feTile>
<feImage width="4" height="4" xlink:href="#dot7"></feImage>
<feTile result="dot7-tile"></feTile>
<feImage width="4" height="4" xlink:href="#dot8"></feImage>
<feTile result="dot8-tile"></feTile>
<!-- Luminance mapping -->
<feColorMatrix in="SourceGraphic" type="luminanceToAlpha" result="lum"></feColorMatrix>
<feComponentTransfer in="lum" result="lum-map">
<feFuncA type="table" tableValues="1 0"></feFuncA>
</feComponentTransfer>
<!-- Discrete thresholds -->
<feComponentTransfer in="lum-map" result="thresh1">
<feFuncA type="discrete" tableValues="1 0 0 0 0 0 0 0"></feFuncA>
</feComponentTransfer>
<feComponentTransfer in="lum-map" result="thresh2">
<feFuncA type="discrete" tableValues="0 1 0 0 0 0 0 0"></feFuncA>
</feComponentTransfer>
<feComponentTransfer in="lum-map" result="thresh3">
<feFuncA type="discrete" tableValues="0 0 1 0 0 0 0 0"></feFuncA>
</feComponentTransfer>
<feComponentTransfer in="lum-map" result="thresh4">
<feFuncA type="discrete" tableValues="0 0 0 1 0 0 0 0"></feFuncA>
</feComponentTransfer>
<feComponentTransfer in="lum-map" result="thresh5">
<feFuncA type="discrete" tableValues="0 0 0 0 1 0 0 0"></feFuncA>
</feComponentTransfer>
<feComponentTransfer in="lum-map" result="thresh6">
<feFuncA type="discrete" tableValues="0 0 0 0 0 1 0 0"></feFuncA>
</feComponentTransfer>
<feComponentTransfer in="lum-map" result="thresh7">
<feFuncA type="discrete" tableValues="0 0 0 0 0 0 1 0"></feFuncA>
</feComponentTransfer>
<feComponentTransfer in="lum-map" result="thresh8">
<feFuncA type="discrete" tableValues="0 0 0 0 0 0 0 1"></feFuncA>
</feComponentTransfer>
<!-- Combine dots with thresholds -->
<feComposite in="thresh1" in2="dot1-tile" operator="in" result="level1"></feComposite>
<feComposite in="thresh2" in2="dot2-tile" operator="in" result="level2"></feComposite>
<feComposite in="thresh3" in2="dot3-tile" operator="in" result="level3"></feComposite>
<feComposite in="thresh4" in2="dot4-tile" operator="in" result="level4"></feComposite>
<feComposite in="thresh5" in2="dot5-tile" operator="in" result="level5"></feComposite>
<feComposite in="thresh6" in2="dot6-tile" operator="in" result="level6"></feComposite>
<feComposite in="thresh7" in2="dot7-tile" operator="in" result="level7"></feComposite>
<feComposite in="thresh8" in2="dot8-tile" operator="in" result="level8"></feComposite>
<!-- Merge everything -->
<feMerge result="merged">
<feMergeNode in="level8"></feMergeNode>
<feMergeNode in="level7"></feMergeNode>
<feMergeNode in="level6"></feMergeNode>
<feMergeNode in="level5"></feMergeNode>
<feMergeNode in="level4"></feMergeNode>
<feMergeNode in="level3"></feMergeNode>
<feMergeNode in="level2"></feMergeNode>
<feMergeNode in="level1"></feMergeNode>
</feMerge>
<!-- Clip to original shape -->
<feComposite in="merged" in2="SourceGraphic" operator="in" result="masked"></feComposite>
<!-- Apply color (optional) -->
<feFlood id="floodHd" flood-color="light-dark(var(--reveal,white),white)" result="color"></feFlood>
<feComposite in="color" in2="masked" operator="in" result="finalHalftone"></feComposite>
<!-- Control the opacity of the halftone pattern using a CSS variable -->
<!-- Control the opacity of the halftone pattern using a CSS variable -->
<feComponentTransfer in="finalHalftone" result="fadedHalftone">
<feFuncA type="linear" slope="var(--halftone-intensity, 1)" />
</feComponentTransfer>
<!-- Place the original image on the bottom and the faded halftone on top -->
<feMerge>
<feMergeNode in="SourceGraphic" />
<feMergeNode in="fadedHalftone" />
</feMerge>
</filter>
</defs>
</svg>
import * as THREE from "three";
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
import * as CANNON from "cannon";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
/* ======================= CONFIG ======================= */
const ROWS = 6;
const COLS = 4;
const WORLD_H = 2.4;
const TARGET_RATIO = 10 / 16;
const WORLD_W = WORLD_H * TARGET_RATIO;
const PW = WORLD_W / COLS;
const PH = WORLD_H / ROWS;
// ====== CUSTOM DEPTH ======
const THICK = 0.2;
const JITTER = 0.05;
const NECK_WIDTH_F = 0.3;
const TAB_SIZE_F = 0.25;
const BOARD_Z = -0.05;
const FLOOR_Y = 0.25;
/* ======================= SETUP ======================= */
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance"
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.domElement.style.touchAction = "none";
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x14161a);
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(
new RoomEnvironment(renderer),
0.04
).texture;
const camera = new THREE.PerspectiveCamera(
35,
innerWidth / innerHeight,
0.1,
100
);
camera.position.set(0, WORLD_H / 2, 5.5);
camera.lookAt(0, WORLD_H / 2, 0);
/* ======================= POST-PROCESSING ======================= */
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const outlinePass = new OutlinePass(
new THREE.Vector2(innerWidth, innerHeight),
scene,
camera
);
outlinePass.edgeStrength = 8;
outlinePass.edgeGlow = 1;
outlinePass.edgeThickness = 3;
outlinePass.visibleEdgeColor.set("#00ff00");
composer.addPass(outlinePass);
composer.addPass(new OutputPass());
/* ======================= PHYSICS ======================= */
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0),
allowSleep: true
});
world.solver.iterations = 20; // Increased for better stability
const GROUP_FLOOR = 1 << 0;
const GROUP_TRAY = 1 << 1; // Renamed for clarity
const GROUP_PIECE = 1 << 2;
const floorBody = new CANNON.Body({
type: CANNON.Body.STATIC,
shape: new CANNON.Plane(),
position: new CANNON.Vec3(0, FLOOR_Y, 0)
});
floorBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
floorBody.collisionFilterGroup = GROUP_FLOOR;
floorBody.collisionFilterMask = GROUP_PIECE;
world.addBody(floorBody);
// ====== SOLID TRAY BEHIND THE BOARD To avoid CRAZY Ghost ======
const trayBody = new CANNON.Body({
type: CANNON.Body.STATIC,
// A thick box to prevent pieces from passing through
shape: new CANNON.Box(new CANNON.Vec3(WORLD_W / 2, WORLD_H / 2, THICK * 5)),
position: new CANNON.Vec3(0, WORLD_H / 2, BOARD_Z - THICK * 4)
});
trayBody.collisionFilterGroup = GROUP_TRAY;
trayBody.collisionFilterMask = GROUP_PIECE; // In case It collides with pieces
world.addBody(trayBody);
/* ======================= BOARD BACKGROUND ======================= */
const boardGeometry = new THREE.PlaneGeometry(WORLD_W, WORLD_H);
const boardMaterial = new THREE.MeshStandardMaterial({
color: 0x0a0a0c,
roughness: 0.8
});
const boardMesh = new THREE.Mesh(boardGeometry, boardMaterial);
boardMesh.position.set(0, WORLD_H / 2, BOARD_Z);
boardMesh.receiveShadow = true;
scene.add(boardMesh);
boardMesh.updateMatrixWorld(true);
const boardBox = new THREE.Box3().setFromObject(boardMesh);
const boardPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), -BOARD_Z);
/* ======================= LIGHTS ======================= */
scene.add(new THREE.HemisphereLight(0xffffff, 0x2f3440, 0.9));
const dir = new THREE.DirectionalLight(0xffffff, 2.5); // Brighter light for glass effect
dir.position.set(3, 5, 4);
dir.castShadow = true;
dir.shadow.mapSize.set(2048, 2048);
scene.add(dir);
const floorMesh = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.ShadowMaterial({ opacity: 0.25 })
);
floorMesh.rotation.x = -Math.PI / 2;
floorMesh.position.y = FLOOR_Y;
floorMesh.receiveShadow = true;
scene.add(floorMesh);
/* ======================= LOAD TEXTURE ======================= */
const imageUrl =
"https://assets.codepen.io/453571/halloween-pumpkins-smiling.avif";
const mainTexture = await new THREE.TextureLoader().loadAsync(imageUrl);
mainTexture.colorSpace = THREE.SRGBColorSpace;
mainTexture.flipY = false;
/*document.getElementById("preview-image")?.setAttribute("src", imageUrl); loaded direcly in html*/
// ====== ENHANCEMENT: GLASS-LIKE MATERIAL ======
const textureMaterial = new THREE.MeshPhysicalMaterial({
map: mainTexture,
roughness: 0.1, // More glossy
metalness: 0.0,
transmission: 0.8, // Semi-transparent
ior: 1.5, // Index of Refraction for glass
thickness: THICK, // Required for transmission to look right
side: THREE.DoubleSide
});
/* ======================= EDGE CURVES ======================= */
const rand = (() => {
const array = new Uint32Array(1);
return () => {
crypto.getRandomValues(array);
return array[0] / 4294967296;
};
})();
const alea = (min, max) => min + (max - min) * rand();
// Store the boolean type of each edge (true=tab) for physics shape generation
const edgeTypes = {
horizontal: Array.from({ length: ROWS - 1 }, () =>
Array.from({ length: COLS }, () => rand() < 0.5)
),
vertical: Array.from({ length: ROWS }, () =>
Array.from({ length: COLS - 1 }, () => rand() < 0.5)
)
};
// Generate the curve functions from the types
function generateEdgeCurve(isTab) {
const sign = isTab ? 1 : -1;
const jitter = alea(1 - JITTER, 1 + JITTER);
return (t, length, perpSign) => {
const neckWidth = length * NECK_WIDTH_F;
const tabDepth = length * TAB_SIZE_F * sign * jitter * perpSign;
if (t < 0.5 - neckWidth / length / 2 || t > 0.5 + neckWidth / length / 2)
return { along: t, perp: 0 };
const localT = (t - (0.5 - neckWidth / length / 2)) / (neckWidth / length);
return {
along: t,
perp: 4 * Math.abs(tabDepth) * localT * (1 - localT) * Math.sign(tabDepth)
};
};
}
const edgeCurves = {
horizontal: edgeTypes.horizontal.map((row) =>
row.map((isTab) => generateEdgeCurve(isTab))
),
vertical: edgeTypes.vertical.map((row) =>
row.map((isTab) => generateEdgeCurve(isTab))
)
};
/* ======================= CREATE PIECE SHAPE ======================= */
function createPieceShape(col, row) {
const shape = new THREE.Shape();
const segments = 32;
function addCurvedEdge(x1, y1, x2, y2, curveFunc, perpSign) {
if (!curveFunc) {
shape.lineTo(x2, y2);
return;
}
const dx = x2 - x1,
dy = y2 - y1,
length = Math.sqrt(dx * dx + dy * dy);
const dirX = dx / length,
dirY = dy / length,
perpX = -dirY * perpSign,
perpY = dirX * perpSign;
for (let i = 1; i <= segments; i++) {
const t = i / segments,
curve = curveFunc(t, length, 1);
shape.lineTo(
x1 + dirX * curve.along * length + perpX * curve.perp,
y1 + dirY * curve.along * length + perpY * curve.perp
);
}
}
shape.moveTo(-PW / 2, -PH / 2);
addCurvedEdge(
-PW / 2,
-PH / 2,
PW / 2,
-PH / 2,
row === ROWS - 1 ? null : edgeCurves.horizontal[row][col],
1
);
addCurvedEdge(
PW / 2,
-PH / 2,
PW / 2,
PH / 2,
col === COLS - 1 ? null : edgeCurves.vertical[row][col],
1
);
addCurvedEdge(
PW / 2,
PH / 2,
-PW / 2,
PH / 2,
row === 0 ? null : edgeCurves.horizontal[row - 1][col],
-1
);
addCurvedEdge(
-PW / 2,
PH / 2,
-PW / 2,
-PH / 2,
col === 0 ? null : edgeCurves.vertical[row][col - 1],
-1
);
return shape;
}
/* ======================= BUILD PIECES ======================= */
const pieces = [];
function buildPiece(col, row) {
const shape = createPieceShape(col, row);
// ====== ENHANCEMENT: DEEPER EMBOSS EFFECT ======
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: THICK * 0.03,
bevelEnabled: true,
bevelThickness: THICK * 0.2,
bevelSize: THICK * 0.2,
bevelSegments: 20
});
const positions = geometry.getAttribute("position");
const uvs = new Float32Array(positions.count * 2);
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i),
y = positions.getY(i);
const localU = (x + PW / 2) / PW,
localV = (y + PH / 2) / PH;
uvs[i * 2] = (col + localU) / COLS;
uvs[i * 2 + 1] = (row + (1.0 - localV)) / ROWS;
}
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
geometry.computeVertexNormals();
const mesh = new THREE.Mesh(geometry, textureMaterial);
mesh.castShadow = true;
mesh.receiveShadow = true;
// ====== ENHANCEMENT: STABLE COMPOUND PHYSICS SHAPE ======
const body = new CANNON.Body({ mass: 0.15, sleepTimeLimit: 0.5 });
// 1. Add the main center box
const centerShape = new CANNON.Box(
new CANNON.Vec3(PW * 0.48, PH * 0.48, THICK / 2)
);
body.addShape(centerShape, new CANNON.Vec3(0, 0, 0));
// 2. Add smaller boxes for the tabs, if they exist
const tabWidth = PW * NECK_WIDTH_F;
const tabHeight = PH * TAB_SIZE_F;
const tabHeightV = PH * NECK_WIDTH_F;
const tabWidthV = PW * TAB_SIZE_F;
if (row > 0 && edgeTypes.horizontal[row - 1][col]) {
body.addShape(
new CANNON.Box(new CANNON.Vec3(tabWidth / 2, tabHeight / 2, THICK / 2)),
new CANNON.Vec3(0, PH / 2, 0)
);
}
if (row < ROWS - 1 && edgeTypes.horizontal[row][col]) {
body.addShape(
new CANNON.Box(new CANNON.Vec3(tabWidth / 2, tabHeight / 2, THICK / 2)),
new CANNON.Vec3(0, -PH / 2, 0)
);
}
if (col < COLS - 1 && edgeTypes.vertical[row][col]) {
body.addShape(
new CANNON.Box(new CANNON.Vec3(tabWidthV / 2, tabHeightV / 2, THICK / 2)),
new CANNON.Vec3(PW / 2, 0, 0)
);
}
if (col > 0 && edgeTypes.vertical[row][col - 1]) {
body.addShape(
new CANNON.Box(new CANNON.Vec3(tabWidthV / 2, tabHeightV / 2, THICK / 2)),
new CANNON.Vec3(-PW / 2, 0, 0)
);
}
body.linearDamping = 0.5;
body.angularDamping = 0.5;
body.collisionFilterGroup = GROUP_PIECE;
// Make sure pieces collide with the floor, other pieces, AND the new tray
body.collisionFilterMask = GROUP_FLOOR | GROUP_PIECE | GROUP_TRAY;
return { mesh, body };
}
/* ======================= CREATE ALL PIECES ======================= */
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const { mesh, body } = buildPiece(col, row);
const targetX = -WORLD_W / 2 + PW / 2 + col * PW;
const targetY = WORLD_H - PH / 2 - row * PH;
const targetZ = BOARD_Z + THICK / 2;
const targetPos = new THREE.Vector3(targetX, targetY, targetZ);
body.position.set(
alea(-WORLD_W / 2, WORLD_W / 2),
FLOOR_Y + 0.3 + Math.random() * 0.8,
0.8 + Math.random() * 0.7
);
body.quaternion.setFromEuler(Math.random(), Math.random(), Math.random()); // Random initial rotation
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
world.addBody(body);
scene.add(mesh);
const piece = {
mesh,
body,
grid: { col, row },
targetPos,
isSolved: false
};
pieces.push(piece);
mesh.userData.piece = piece;
}
}
/* ======================= AAAARGH Still missing a clean collision) ======================= */
const raycaster = new THREE.Raycaster();
raycaster.near = 0.1;
raycaster.far = 100;
const freeDragPlane = new THREE.Plane();
const tmpVec = new THREE.Vector3();
let draggedPiece = null;
let dragOffset = new THREE.Vector3();
const originalMasks = new WeakMap();
function clampToBoardXY(v) {
const marginX = PW * 0.48;
const marginY = PH * 0.48;
v.x = Math.max(-WORLD_W / 2 + marginX, Math.min(WORLD_W / 2 - marginX, v.x));
v.y = Math.max(marginY, Math.min(WORLD_H - marginY, v.y));
return v;
}
function getGridSlotFromWorldPos(worldPos) {
if (Math.abs(worldPos.z - (BOARD_Z + THICK / 2)) > THICK * 2) return null;
const col = Math.round((worldPos.x - (-WORLD_W / 2 + PW / 2)) / PW);
const row = Math.round((WORLD_H - PH / 2 - worldPos.y) / PH);
if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return null;
return { col, row };
}
function isNearTarget(piece, pos, tol = Math.min(PW, PH) * 0.15) {
return (
Math.abs(pos.x - piece.targetPos.x) <= tol &&
Math.abs(pos.y - piece.targetPos.y) <= tol
);
}
function onPointerDown(e) {
if (draggedPiece) return;
const ndc = {
x: (e.clientX / innerWidth) * 2 - 1,
y: -(e.clientY / innerHeight) * 2 + 1
};
raycaster.setFromCamera(ndc, camera);
const unsolved = pieces.filter((p) => !p.isSolved).map((p) => p.mesh);
const hits = raycaster.intersectObjects(unsolved);
if (hits.length > 0) {
draggedPiece = hits[0].object.userData.piece;
draggedPiece.body.type = CANNON.Body.KINEMATIC;
originalMasks.set(draggedPiece.body, {
group: draggedPiece.body.collisionFilterGroup,
mask: draggedPiece.body.collisionFilterMask
});
draggedPiece.body.collisionFilterMask = 0;
camera.getWorldDirection(tmpVec);
tmpVec.normalize();
freeDragPlane.setFromNormalAndCoplanarPoint(tmpVec.negate(), hits[0].point);
dragOffset.copy(hits[0].point).sub(draggedPiece.mesh.position);
}
}
function onPointerMove(e) {
if (!draggedPiece) return;
const ndc = {
x: (e.clientX / innerWidth) * 2 - 1,
y: -(e.clientY / innerHeight) * 2 + 1
};
raycaster.setFromCamera(ndc, camera);
const pBoard = new THREE.Vector3();
const hitBoard = raycaster.ray.intersectPlane(boardPlane, pBoard);
const isOverBoard =
!!hitBoard &&
boardBox.containsPoint(new THREE.Vector3(pBoard.x, pBoard.y, BOARD_Z));
const pFree = new THREE.Vector3();
if (!isOverBoard) raycaster.ray.intersectPlane(freeDragPlane, pFree);
const intersectionPoint = isOverBoard ? pBoard : pFree;
if (!intersectionPoint) return;
let newPos = intersectionPoint.sub(dragOffset);
if (isOverBoard) {
newPos.z = BOARD_Z + THICK / 2;
clampToBoardXY(newPos);
} else {
newPos.z = Math.max(BOARD_Z + THICK / 2 + 0.05, newPos.z);
}
draggedPiece.body.position.copy(newPos);
draggedPiece.body.quaternion.setFromEuler(0, 0, 0);
const targetSlot = getGridSlotFromWorldPos(newPos);
const inSlot =
targetSlot &&
targetSlot.col === draggedPiece.grid.col &&
targetSlot.row === draggedPiece.grid.row;
const near = isNearTarget(draggedPiece, newPos, Math.min(PW, PH) * 0.2);
outlinePass.selectedObjects =
isOverBoard && inSlot && near ? [draggedPiece.mesh] : [];
}
function onPointerUp() {
if (!draggedPiece) return;
const currentPos = new THREE.Vector3().copy(draggedPiece.body.position);
const targetSlot = getGridSlotFromWorldPos(currentPos);
let placed = false;
const correctSlot =
targetSlot &&
targetSlot.col === draggedPiece.grid.col &&
targetSlot.row === draggedPiece.grid.row;
const nearTarget = isNearTarget(draggedPiece, currentPos);
if (correctSlot && nearTarget) {
draggedPiece.body.type = CANNON.Body.STATIC;
draggedPiece.body.position.copy(draggedPiece.targetPos);
draggedPiece.body.quaternion.setFromEuler(0, 0, 0);
draggedPiece.isSolved = true;
placed = true;
}
const saved = originalMasks.get(draggedPiece.body);
if (saved) {
draggedPiece.body.collisionFilterGroup = saved.group;
draggedPiece.body.collisionFilterMask = saved.mask;
originalMasks.delete(draggedPiece.body);
}
if (!placed) {
draggedPiece.body.type = CANNON.Body.DYNAMIC;
draggedPiece.body.position.z = Math.max(
BOARD_Z + THICK / 2 + 0.05,
draggedPiece.body.position.z
);
draggedPiece.body.wakeUp();
}
outlinePass.selectedObjects = [];
draggedPiece = null;
}
window.addEventListener("pointerdown", onPointerDown, { passive: true });
window.addEventListener("pointermove", onPointerMove, { passive: true });
window.addEventListener("pointerup", onPointerUp, { passive: true });
/* ======================= ANIMATION LOOP ======================= */
const clock = new THREE.Clock();
function tick() {
const dt = clock.getDelta();
world.step(1 / 60, dt, 3);
for (const p of pieces) {
p.mesh.position.copy(p.body.position);
p.mesh.quaternion.copy(p.body.quaternion);
}
composer.render();
requestAnimationFrame(tick);
}
tick();
/* ======================= RESIZE ======================= */
window.addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
composer.setSize(innerWidth, innerHeight);
});
:root,
body {
height: 100%;
margin: 0;
background: #0b0b0b;
}
canvas {
display: block;
}
/*preview*/
#preview-container {
position: absolute;
top: 25%;
right: 20px;
width: 15%;
max-width: 180px;
background: light-dark(white, black);
border: 3px solid #fff;
border-radius: 5px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.7);
z-index: 2;
& #preview-image {
width: 100%;
height: auto;
display: block;
object-fit: cover;
filter: url("#half-tone-filter");
-webkit-filter: url("#half-tone-filter");
transition: --halftone-intensity 0.5s ease-in-out;
/* Set the initial intensity to 0 */
--halftone-intensity: 1;
--reveal: white;
&:hover {
transition: all 0.5s allow-discrete;
--halftone-intensity: 0;
--reveal: black;
}
}
}
/*swtich*/
input[type="checkbox"] {
appearance: none;
visibility: hidden;
position: absolute;
left: -100vw;
}
.cheat_reveal {
position: absolute;
top: calc(5% + 1vw);
right: 20px;
color: white;
cursor: pointer;
width: 180px;
max-width: 180px;
height: 50px;
background: grey;
border-radius: 100px;
display: flex;
place-content: center;
place-items: center;
& .view-reference_txt {
font: 12px/1.35 system-ui, -apple-system, sans-serif;
position: absolute;
top: -50%;
}
}
label:after {
content: "";
position: absolute;
inset-block: 0;
margin-block: auto;
left: 5px;
width: 45px;
height: 45px;
background: #fff;
border-radius: calc(1px / 0);
transition: 0.3s;
}
input:checked + label {
background: #bada55;
}
input:checked + label:after {
left: calc(100% - 5px);
transform: translateX(-100%);
}
label:active:after {
width: 130px;
}
/*diclaimer*/
/* I tried handle variables via CSS but .. I ended via the SVG itself*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment