點選圖片 → 載入至 Canvas → 滑鼠懸停/拖曳控制圖片旋轉、縮放、平移。
mousedown/mousemove/mouseup/wheel。requestAnimationFrame 或事件觸發後 draw()。<!DOCTYPE html>
<html><head><meta charset="utf-8"><style>
body { margin:0; overflow:hidden; background:#222; }
canvas { display:block; cursor:grab; background:#111; }
canvas:active { cursor:grabbing; }
#imgPicker { position:fixed; top:10px; left:10px; z-index:10; }
</style></head><body>
<input type="file" id="imgPicker" accept="image/*">
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let img = null;
// 變換矩陣 [a, b, c, d, e, f] 對應 DOMMatrix 2D
let mat = [1, 0, 0, 1, 0, 0]; // identity
let dragging = false, lastX = 0, lastY = 0;
// 矩陣工具
const mMult = (m, n) => [
m[0]*n[0] + m[1]*n[2], m[0]*n[1] + m[1]*n[3],
m[2]*n[0] + m[3]*n[2], m[2]*n[1] + m[3]*n[3],
m[4]*n[0] + m[5]*n[2] + n[4], m[4]*n[1] + m[5]*n[3] + n[5]
];
const mTranslate = (tx, ty) => [1,0,0,1,tx,ty];
const mScale = (sx, sy) => [sx,0,0,sy,0,0];
const mRotate = (ang) => { const c=Math.cos(ang), s=Math.sin(ang); return [c,s,-s,c,0,0]; };
const mInverse = (m) => {
const det = m[0]*m[3] - m[1]*m[2];
return [ m[3]/det, -m[1]/det, -m[2]/det, m[0]/det,
(m[2]*m[5]-m[3]*m[4])/det, (m[1]*m[4]-m[0]*m[5])/det ];
};
// 將滑鼠座標轉為圖片局部座標
function toLocal(x, y) {
const inv = mInverse(mat);
return { x: inv[0]*x + inv[2]*y + inv[4], y: inv[1]*x + inv[3]*y + inv[5] };
}
// 重繪
function draw() {
const w = canvas.width = innerWidth, h = canvas.height = innerHeight;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,w,h);
if (!img) return;
ctx.setTransform(...mat);
ctx.drawImage(img, 0, 0);
}
// 事件
canvas.addEventListener('mousedown', e => {
dragging = true; lastX = e.clientX; lastY = e.clientY;
canvas.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', e => {
if (!dragging) return;
const dx = e.clientX - lastX, dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
// 平移:在當前變換下平移
mat = mMult(mTranslate(dx, dy), mat);
draw();
});
window.addEventListener('mouseup', () => { dragging = false; canvas.style.cursor = 'grab'; });
canvas.addEventListener('wheel', e => {
e.preventDefault();
const scale = e.deltaY > 0 ? 0.9 : 1.1;
// 以滑鼠位置為中心縮放:T⁻¹ * S * T
const local = toLocal(e.clientX, e.clientY);
mat = mMult(mMult(mMult(mTranslate(local.x, local.y), mScale(scale, scale)), mTranslate(-local.x, -local.y)), mat);
draw();
}, {passive:false});
// 載入圖片
document.getElementById('imgPicker').addEventListener('change', e => {
const file = e.target.files[0]; if (!file) return;
const url = URL.createObjectURL(file);
img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
// 置中且適應畫面
const scale = Math.min(innerWidth/img.width, innerHeight/img.height) * 0.9;
mat = [scale,0,0,scale, (innerWidth-img.width*scale)/2, (innerHeight-img.height*scale)/2];
draw();
};
img.src = url;
});
// 處理視窗縮放
window.addEventListener('resize', draw);
</script></body></html>
mat = mMult(newT, mat)),符合「先套用新變換,再套用舊變換」的語意。T⁻¹),套用縮放/旋轉,再移回(T),即 T * S * T⁻¹。toLocal() 後檢查 0 <= x < img.width && 0 <= y < img.height。draw(),避免持續 requestAnimationFrame。canvas.add(new fabric.Image(img)) 內建旋轉/縮放/移動控制點。Transformer。<img> 置於 <div>,用 CSS transform: matrix(...),效能極佳但需自行處理座標映射。