Source: modules/physics.js

/**
 * Provides functions for collision detection.
 */
export class Physics {

    /**
     * Checks if two axis-aligned rectangles overlap.
     * @param {number} x1 X-coordinate of the first rectangle.
     * @param {number} y1 Y-coordinate of the first rectangle.
     * @param {number} w1 Width of the first rectangle.
     * @param {number} h1 Height of the first rectangle.
     * @param {number} x2 X-coordinate of the second rectangle.
     * @param {number} y2 Y-coordinate of the second rectangle.
     * @param {number} w2 Width of the second rectangle.
     * @param {number} h2 Height of the second rectangle.
     * @returns {boolean} True if the rectangles overlap, false otherwise.
     */
    rectsOverlap(x1, y1, w1, h1, x2, y2, w2, h2) {
        // AABB collision detection
        return x1 < x2 + w2 &&
               x1 + w1 > x2 &&
               y1 < y2 + h2 &&
               y1 + h1 > y2;
    }

	/**
	 * @private
	 * Calculates the axis-aligned bounding box (AABB) for a transformed image.
	 * @param {Jmage} img The image object.
	 * @param {number} x The world x-coordinate of the image's handle.
	 * @param {number} y The world y-coordinate of the image's handle.
	 * @returns {{x: number, y: number, width: number, height: number}} The AABB.
	 */
	#getTransformedBoundingBox(img, x, y) {
		const w = img.width;
		const h = img.height;
		const hx = img.handle.x;
		const hy = img.handle.y;
		const sx = img.scaleFactor.x;
		const sy = img.scaleFactor.y;
		const rad = img.rotation * Math.PI / 180;
		const cos = Math.cos(rad);
		const sin = Math.sin(rad);

		const corners = [
			{ x: -hx, y: -hy },
			{ x: w - hx, y: -hy },
			{ x: w - hx, y: h - hy },
			{ x: -hx, y: h - hy }
		];

		let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;

		corners.forEach(corner => {
			const scaledX = corner.x * sx;
			const scaledY = corner.y * sy;

			const rotatedX = scaledX * cos - scaledY * sin;
			const rotatedY = scaledX * sin + scaledY * cos;

			const finalX = rotatedX + x;
			const finalY = rotatedY + y;

			minX = Math.min(minX, finalX);
			maxX = Math.max(maxX, finalX);
			minY = Math.min(minY, finalY);
			maxY = Math.max(maxY, finalY);
		});

		return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
	}

    /**
     * Checks if two images, placed at specific coordinates, overlap based on their bounding boxes.
     * This check correctly handles scaled and rotated images by calculating their axis-aligned bounding box.
     * @param {Jmage} img1 The first image object.
     * @param {number} x1 The x-coordinate of the first image's handle.
     * @param {number} y1 The y-coordinate of the first image's handle.
     * @param {Jmage} img2 The second image object.
     * @param {number} x2 The x-coordinate of the second image's handle.
     * @param {number} y2 The y-coordinate of the second image's handle.
     * @returns {boolean} True if the images' bounding boxes overlap, false otherwise.
     */
    imagesOverlap(img1, x1, y1, img2, x2, y2) {
        if (!img1 || !img2 || !img1.width || !img2.width) {
            return false; // One of the images is not loaded or invalid
        }

		const box1 = this.#getTransformedBoundingBox(img1, x1, y1);
		const box2 = this.#getTransformedBoundingBox(img2, x2, y2);

		return this.rectsOverlap(box1.x, box1.y, box1.width, box1.height, box2.x, box2.y, box2.width, box2.height);
    }

    /**
     * Checks for pixel-perfect collision between two images, correctly handling translation, scaling, and rotation.
     * @param {Jmage} img1 The first image object.
     * @param {number} x1 The x-coordinate of the first image's handle.
     * @param {number} y1 The y-coordinate of the first image's handle.
     * @param {number} [frame1=1] The frame number for the first image.
     * @param {Jmage} img2 The second image object.
     * @param {number} x2 The x-coordinate of the second image's handle.
     * @param {number} y2 The y-coordinate of the second image's handle.
     * @param {number} [frame2=1] The frame number for the second image.
     * @param {number} [alphaThreshold=128] The alpha value (0-255) above which a pixel is considered solid.
     * @returns {boolean} True if any solid pixels overlap, false otherwise.
     */
    imagesCollide(img1, x1, y1, frame1 = 1, img2, x2, y2, frame2 = 1, alphaThreshold = 128) {
        const box1 = this.#getTransformedBoundingBox(img1, x1, y1);
        const box2 = this.#getTransformedBoundingBox(img2, x2, y2);
        if (!this.rectsOverlap(box1.x, box1.y, box1.width, box1.height, box2.x, box2.y, box2.width, box2.height)) {
            return false;
        }

        const data1 = img1.getImageData();
        const data2 = img2.getImageData();
        if (!data1 || !data2) return false; // Images not ready

        const intersectX1 = Math.max(box1.x, box2.x);
        const intersectY1 = Math.max(box1.y, box2.y);
        const intersectX2 = Math.min(box1.x + box1.width, box2.x + box2.width);
        const intersectY2 = Math.min(box1.y + box1.height, box2.y + box2.height);

        const rad1 = -img1.rotation * Math.PI / 180;
        const cos1 = Math.cos(rad1);
        const sin1 = Math.sin(rad1);
        const invScaleX1 = 1 / img1.scaleFactor.x;
        const invScaleY1 = 1 / img1.scaleFactor.y;

        const rad2 = -img2.rotation * Math.PI / 180;
        const cos2 = Math.cos(rad2);
        const sin2 = Math.sin(rad2);
        const invScaleX2 = 1 / img2.scaleFactor.x;
        const invScaleY2 = 1 / img2.scaleFactor.y;

        const frame1Pos = img1.getFramePos(frame1 - 1);
        const frame2Pos = img2.getFramePos(frame2 - 1);

        for (let y = intersectY1; y < intersectY2; y++) {
            for (let x = intersectX1; x < intersectX2; x++) {
                const dx1 = x - x1, dy1 = y - y1;
                const rotatedX1 = dx1 * cos1 - dy1 * sin1;
                const rotatedY1 = dx1 * sin1 + dy1 * cos1;
                const localX1 = (rotatedX1 * invScaleX1) + img1.handle.x;
                const localY1 = (rotatedY1 * invScaleY1) + img1.handle.y;

                const dx2 = x - x2, dy2 = y - y2;
                const rotatedX2 = dx2 * cos2 - dy2 * sin2;
                const rotatedY2 = dx2 * sin2 + dy2 * cos2;
                const localX2 = (rotatedX2 * invScaleX2) + img2.handle.x;
                const localY2 = (rotatedY2 * invScaleY2) + img2.handle.y;

                if (localX1 >= 0 && localX1 < img1.width && localY1 >= 0 && localY1 < img1.height &&
                    localX2 >= 0 && localX2 < img2.width && localY2 >= 0 && localY2 < img2.height) {

                    const sourceX1 = frame1Pos.x + Math.floor(localX1);
                    const sourceY1 = frame1Pos.y + Math.floor(localY1);
                    const sourceX2 = frame2Pos.x + Math.floor(localX2);
                    const sourceY2 = frame2Pos.y + Math.floor(localY2);

                    const alpha1 = data1.data[(sourceY1 * data1.width + sourceX1) * 4 + 3];
                    const alpha2 = data2.data[(sourceY2 * data2.width + sourceX2) * 4 + 3];

                    if (alpha1 > alphaThreshold && alpha2 > alphaThreshold) {
                        return true; // Collision detected
                    }
                }
            }
        }

        return false;
    }
}