Big restructure to support layer transformations

This commit is contained in:
Neil Graham
2023-12-23 20:09:39 +13:00
parent eac8d3139a
commit 3ab718fe38
5 changed files with 801 additions and 114 deletions
+156 -109
View File
@@ -52,6 +52,7 @@ function setActivePic(newValue) {
if (!activePic) return;
activePic.bringToFront();
if (typeof tool.drawUI === "function") tool.drawUI();
}
var dragging = false;
@@ -88,6 +89,9 @@ function closePic(pic) {
}
}
var lastUsedMaskColor = "#402040";
class Layer {
@@ -104,6 +108,9 @@ class Layer {
this.canvas.height=height;
this.mask = mask;
this.title= title;
this.transform = [1,0 ,0,1, 0,0];
this.rotationCenter= {x:width/2,y:height/2};
if (mask) {
this.compositeOperation="source-over";
}
@@ -111,6 +118,12 @@ class Layer {
get maskColor() {
return this._maskColor;
}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
set maskColor(newValue) {
this._maskColor=newValue;
if (this.mask) {
@@ -121,6 +134,58 @@ class Layer {
this.ctx.restore();
}
}
draw(ctx) {
ctx.save();
ctx.globalCompositeOperation=this.compositeOperation;
ctx.globalAlpha=this.opacity;
ctx.setTransform(...this.transform);
ctx.drawImage(this.canvas);
ctx.restore();
}
convertFromPicCoords(x, y) {
return reverseTransform({x,y},this.transform)
}
// Converts coordinates from this layer's coordinates to the picture's canvas
convertToPicCoords(x, y) {
return applyTransform({x,y},this.transform);
}
rotate(angle) {
const radians = angle * Math.PI / 180;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
// Convert relative coordinates to absolute
const centerX = this.rotationCenter.x;
const centerY = this.rotationCenter.y;
// Create a translation matrix to move the rotation point to (0,0)
const translateToOrigin = [1, 0, 0, 1, -centerX, -centerY];
// Create a rotation matrix
const rotation = [cos, sin, -sin, cos, 0, 0];
// Create a translation matrix to move back from (0,0)
const translateBack = [1, 0, 0, 1, centerX, centerY];
// Concatenate the transforms: translate back -> rotate -> translate to origin
let combined = concatenateTransforms(translateBack, rotation);
combined = concatenateTransforms(combined, translateToOrigin);
// Apply to the current transform
this.transform = concatenateTransforms(this.transform, combined);
}
translate(dx, dy) {
const newTransform = [1, 0, 0, 1, dx, dy];
this.transform = concatenateTransforms(this.transform, newTransform);
}
scale(sx, sy = sx) {
const newTransform = [sx, 0, 0, sy, 0, 0];
this.transform = concatenateTransforms(this.transform, newTransform);
}
}
function updateMirrorGridButtonImage() {
@@ -161,6 +226,17 @@ function updateMirrors() {
mirrorFunction=result;
}
function getDrawAreaAtPoint(uiX, uiY) {
for (let pic of picStack) {
const {x,y} = convertCoordsFromUIToPic(uiX, uiY, pic);
if (x >= 0 && x <= pic.width && y >= 0 && y <= pic.height) {
return pic;
}
}
return null; // No drawArea found at the point
}
function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
const undoDepth = 10;
const element = document.createElement("div")
@@ -214,6 +290,8 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
get canvas() {return canvas},
get mask() {return mask},
set mask(m) {mask=m},
get width() {return canvas.width},
get height() {return canvas.height},
scale:1,
scalefactor:0,
offsetX:0,
@@ -222,7 +300,7 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
activeLayer:null,
strokeCoordinates :[],
strokeModifier: a=>a,
setTransform( ) {
setCSSTransform( ) {
const {element,scale,offsetX,offsetY} = this;
element.style.setProperty("--scalefactor",scale)
element.style.setProperty("--translateX",offsetX+"px")
@@ -231,7 +309,7 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
setPosition(x,y) {
this.offsetX=x;
this.offsetY=y;
this.setTransform();
this.setCSSTransform();
},
bringToFront() {
const oldPos = picStack.indexOf(this);
@@ -258,6 +336,7 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
if (this.isDrawing && layer===this.activeLayer) {
canvas.ctx.drawImage(activeOperationCanvas,0,0);
} else {
canvas.ctx.setTransform(...layer.transform);
canvas.ctx.drawImage(layer.canvas,0,0);
}
@@ -270,6 +349,7 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
const undoRecord = {
type : "layer",
layer: this.activeLayer,
transform: [...this.activeLayer.transform],
data: this.activeLayer.ctx.getAllImageData()
}
undoStack.push(undoRecord);
@@ -277,6 +357,8 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
redoStack.length=0;
this.activeLayer.ctx.putImageData(data,0,0);
this.activeLayer.transform= [1,0 ,0,1, 0,0];
this.isDrawing = false;
if (selectedExport===this) {
if (this.activeLayer===this.mask) {
@@ -337,9 +419,12 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
if (!updatingStroke) {
updatingStroke=true;
setTimeout(_=>{
activeOperationCanvas.ctx.clearRect(0,0,activeOperationCanvas.width,activeOperationCanvas.height);
activeOperationCanvas.ctx.drawImage(this.activeLayer.canvas,0,0);
const ctx = activeOperationCanvas.ctx;
ctx.clearRect(0,0,activeOperationCanvas.width,activeOperationCanvas.height);
ctx.save()
ctx.setTransform(...this.activeLayer.transform);
ctx.drawImage(this.activeLayer.canvas,0,0);
ctx.restore();
const unitRange=this.strokeCoordinates.map(({x,y})=>({x:x/canvas.width,y:y/canvas.height}));
const strokes = this.strokeModifier([unitRange])
@@ -377,63 +462,35 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
layers.length = 0; // Clear the existing array
layers.push(...newList); // Fill with new values
},
/*
removeLayer(layer=this.activeLayer) {
const i = layers.indexOf(layer);
if (i !==-1) {
layers.splice(i,1);
if (layer === this.activeLayer) {
this.activeLayer = layers[Math.min(layers.length-1,i)];
}
}
this.updateVisualRepresentation(true);
},
addEmptyLayer() {
let result = new Layer("new layer", canvas)
layers.push(result);
return result;
},
insertLayer(layer, below = null) {
const indexExisting = layers.indexOf(layer);
if (indexExisting !== -1) {
layers.splice(indexExisting, 1);
}
const indexBelow = layers.indexOf(below);
if (indexBelow !== -1) {
layers.splice(indexBelow, 0, layer);
} else {
layers.push(layer);
}
},
*/
addEmptyLayer() {
addEmptyLayer(above = this.activeLayer) {
const newLayer = new Layer(this,"new layer", canvas);
const newList = [...layers, newLayer];
this.updateLayerList(newList);
this.insertLayerAbove(newLayer, above);
this.activeLayer = newLayer;
return newLayer;
},
addDuplicateLayer(layer) {
addDuplicateLayer(layer,above=layer) {
if (!layer) {
return this.addEmptyLayer();
} else {
if (layers.indexOf(above) < 0) above=this.activeLayer;
// Duplicate the provided layer
const newLayer = new Layer(this, layer.title, {width: layer.canvas.width, height: layer.canvas.height}, layer.mask);
newLayer.ctx.drawImage(layer.canvas, 0, 0); // Copy the content of the original layer
newLayer.transform=[...layer.transform];
newLayer.visible = layer.visible;
newLayer.compositeOperation = layer.compositeOperation;
newLayer.opacity = layer.opacity;
const newList = [...this.layers, newLayer];
this.updateLayerList(newList);
this.insertLayerAbove(newLayer, above);
this.activeLayer = newLayer;
return newLayer;
}
}
},
removeLayer(layer = this.activeLayer) {
const newList = layers.filter(l => l !== layer);
this.updateLayerList(newList);
},
insertLayer(layer, below = null) {
insertLayerBelow(layer, below = null) {
const newList = [...layers];
const indexExisting = newList.indexOf(layer);
if (indexExisting !== -1) {
@@ -446,7 +503,21 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
newList.push(layer);
}
this.updateLayerList(newList);
},
},
insertLayerAbove(layer, above = null) {
const newList = [...this.layers];
const indexExisting = newList.indexOf(layer);
if (indexExisting !== -1) {
newList.splice(indexExisting, 1);
}
const indexAbove = newList.indexOf(above);
if (indexAbove !== -1) {
newList.splice(indexAbove + 1, 0, layer);
} else {
newList.unshift(layer); // Inserts at the top if 'above' layer is not found
}
this.updateLayerList(newList);
},
undo() {
const lastRecord = undoStack.pop();
if (lastRecord) {
@@ -458,9 +529,14 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
redoStack.push({
type: 'layer',
layer: lastRecord.layer,
transform : [...lastRecord.layer.transform],
data: lastRecord.layer.ctx.getAllImageData()
});
lastRecord.layer.ctx.putImageData(lastRecord.data, 0, 0);
const ctx = lastRecord.layer.ctx;
ctx.save();
ctx.setTransform(...lastRecord.transform);
ctx.putImageData(lastRecord.data, 0, 0);
ctx.restore();
}
//... (handle other types of undoRecords)
}
@@ -479,6 +555,7 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
undoStack.push({
type: 'layer',
layer: lastRecord.layer,
transform: [...lastRecord.layer.transform],
data: lastRecord.layer.ctx.getAllImageData()
});
lastRecord.layer.ctx.putImageData(lastRecord.data, 0, 0);
@@ -531,7 +608,7 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
})
});
pic.setTransform();
pic.setCSSTransform();
setActivePic(pic)
return pic;
@@ -556,8 +633,11 @@ function createDrawArea(canvas = blankCanvas(),initialTitle="Image") {
function initPaint(){
$("#background").val("#ffffff").data("eraser",true);
// Call this function at initialization
window.uiCanvas = initUICanvas();
$("#background").val("#ffffff").data("eraser",true);
var palette=$("#palette");
for (let i of initialPalette) {
const e=`<div class="paletteentry" style="background-color:${i}"> </div>`;
@@ -606,6 +686,7 @@ function initPaint(){
$("#eraser")[0].tool=eraserTip;
$("#fine_eraser")[0].tool=pixelClear;
$("#eyedropper")[0].tool=eyeDropper;
$("#transform")[0].tool=transformTool;
$(".tool.button").on("click",function(e) {setTool(e.currentTarget.tool);});
@@ -736,7 +817,7 @@ function addNewLayer(image) {
layer.ctx.drawImage(image,0,0);
console.log("new layer", layer)
console.log(layer.canvas.width, layer.canvas.height)
pic.insertLayer(layer)
pic.insertLayerBelow(layer)
pic.activeLayer=layer;
updateLayerList();
pic.updateVisualRepresentation(true);
@@ -826,7 +907,7 @@ function handleMouseLeave(e) {
console.log({dx,dy})
pic.offsetX=dragStartX+dx;
pic.offsetY=dragStartY+dy;
pic.setTransform();
pic.setCSSTransform();
}
}
@@ -862,7 +943,7 @@ function handleMouseMove(e) {
var dy = e.clientY-mouseDownY;
pic.offsetX=dragStartX+dx;
pic.offsetY=dragStartY+dy;
pic.setTransform();
pic.setCSSTransform();
}
if (pic.isDrawing && e.buttons === 0) {
@@ -880,7 +961,7 @@ function handleMouseWheel(e) {
const scaleAround= {x:e.pageX,y:e.pageY};
setScale(activePic.scalefactor+direction,scaleAround);
activePic.setTransform();
activePic.setCSSTransform();
}
function scalefactorToSize(n) {
@@ -910,12 +991,20 @@ function setScale(newfactor, around) {
pic.scale=newScale;
pic.scalefactor=newfactor;
pic.setTransform();
pic.setCSSTransform();
updateBrushCursor(pic.element);
}
function setTool(newValue) {
tool=newValue;
if (newValue !== tool)
{
tool=newValue;
const ctx=uiCanvas.getContext("2d");
ctx.clearRect(0,0,1e5,1e5);
if (typeof tool?.init === "function") tool.init();
}
for (const pic of picStack) {
updateBrushCursor(pic.element)
}
@@ -926,6 +1015,8 @@ function setTool(newValue) {
e.classList.toggle("down",(e.tool === tool))
};
brushSizeControl.diameter=tip.size;
uiCanvas.style.pointerEvents=tool?.eventHandlers?"":"none";
}
@@ -1124,7 +1215,7 @@ function poulateLayerControl() {
if (draggingElement) {
const layer = draggingElement.layer;
const below = insertPoint?.layer;
activePic.insertLayer(layer,below);
activePic.insertLayerBelow(layer,below);
updateLayerList();
activePic.updateVisualRepresentation(true);
} else {
@@ -1185,7 +1276,10 @@ function updateLayerList() {
window.dummyGlobal=result;
const canvas = result.querySelector(".thumbnail");
const ctx=canvas.getContext("2d");
ctx.save()
ctx.setTransform(...layer.transform);
ctx.drawImage(layer.canvas,0,0,canvas.width,canvas.height);
ctx.restore();
result.layer=layer;
// Renaming handler
@@ -1291,63 +1385,11 @@ function updateBrushCursor(picElement) {
let value ="crosshair";
if (tool.cursorFunction) value = tool.cursorFunction(scale);
p.style.setProperty("cursor",value);
//set matching cursor for user interface overlay;
uiCanvas.style.setProperty("cursor",value);
}
function cloneEvent(event, modifications = {}) {
let eventProperties = {
"bubbles": event.bubbles,
"cancelBubble": event.cancelBubble,
"cancelable": event.cancelable,
"composed": event.composed,
"currentTarget": event.currentTarget,
"defaultPrevented": event.defaultPrevented,
"eventPhase": event.eventPhase,
"isTrusted": event.isTrusted,
"target": event.target,
"timeStamp": event.timeStamp,
"type": event.type,
};
// Checking if the event is a MouseEvent, add mouse event properties
if(event instanceof MouseEvent) {
eventProperties = {
...eventProperties,
"altKey": event.altKey,
"button": event.button,
"buttons": event.buttons,
"clientX": event.clientX,
"clientY": event.clientY,
"ctrlKey": event.ctrlKey,
"metaKey": event.metaKey,
"movementX": event.movementX,
"movementY": event.movementY,
"offsetX": event.offsetX,
"offsetY": event.offsetY,
"pageX": event.pageX,
"pageY": event.pageY,
"relatedTarget": event.relatedTarget,
"screenX": event.screenX,
"screenY": event.screenY,
"shiftKey": event.shiftKey,
};
}
return { ...modifications, ...eventProperties };
}
function forwardEvent(mouseEvent, recipient) {
var rect = recipient.getBoundingClientRect();
const options = cloneEvent(mouseEvent,{
clientX:mouseEvent.clientX - rect.left,
clientY:mouseEvent.clientY - rect.top,
});
var newEvent = new MouseEvent(mouseEvent.type, options )
recipient.dispatchEvent(newEvent);
}
function createCaptureOverlay(element) {
var overlay = document.createElement('div');
@@ -1406,3 +1448,8 @@ hotkeys["E"] = _=>{setTool(eraserTip);}
hotkeys["]"] = _=>{brushSizeControl.diameter+=1}
hotkeys["["] = _=>{brushSizeControl.diameter-=1;}
hotkeys["Q"] = _=>{
drawPicFramesOnUICanvas(uiCanvas, picStack);
}
+6 -5
View File
@@ -24,11 +24,12 @@
<option value="ignore">Ignore</option>
</select>
</div>
<div id="pen" title="brush 🇧" class="tool button"></div>
<div id="eraser" title="eraser 🇪" class="tool button" style="background-image:url(images/eraser.png);"></div>
<div id="pixels" title="pixels 🇿" class="tool button" style="background-image:url(images/pixels.png);"></div>
<div id="fine_eraser" title="clear pixels" class="tool button" style="background-image:url(images/fine_eraser.png);"></div>
<div id="eyedropper" title="sample color 🇵" class="tool button" ></div>
<div id="pen" title="Brush 🇧" class="tool button"></div>
<div id="eraser" title="Eraser 🇪" class="tool button" style="background-image:url(images/eraser.png);"></div>
<div id="pixels" title="Pixels 🇿" class="tool button" style="background-image:url(images/pixels.png);"></div>
<div id="fine_eraser" title="Clear pixels" class="tool button" style="background-image:url(images/fine_eraser.png);"></div>
<div id="eyedropper" title="Sample color 🇵" class="tool button" ></div>
<div id="transform" title="Tansform Layer 🇹" class="tool button" ></div>
<div class="subpanel actions">
<div id="clear" class="action button" ></div>
<div id="undo" class="action button" ></div>
+392
View File
@@ -1,3 +1,130 @@
'use strict';
function convertCoordsFromPicToUI(x,y, pic) {
const picRect = pic.element.getBoundingClientRect();
const workspaceRect = document.getElementById('workspace').getBoundingClientRect();
// Scale the point
const scaledX = x * pic.scale;
const scaledY = y * pic.scale;
// Translate the point based on the pic's position in the workspace
const uiX = picRect.left - workspaceRect.left + scaledX;
const uiY = picRect.top - workspaceRect.top + scaledY;
return { x: uiX, y: uiY };
}
function convertCoordsFromUIToPic(x,y, pic) {
const picRect = pic.element.getBoundingClientRect();
const workspaceRect = document.getElementById('workspace').getBoundingClientRect();
// Translate the point to the pic's coordinate system
const picX = x - (picRect.left - workspaceRect.left);
const picY = y - (picRect.top - workspaceRect.top);
// Scale the point based on the pic's scale
const scaledX = picX / pic.scale;
const scaledY = picY / pic.scale;
return { x: scaledX, y: scaledY };
}
function initUICanvas() {
const uiCanvas = document.createElement('canvas');
uiCanvas.id = 'uiCanvas';
uiCanvas.style.position = 'absolute';
uiCanvas.style.left = '0';
uiCanvas.style.top = '0';
uiCanvas.style.zIndex = '101'; // Ensure it's above other elements
const workspace = document.getElementById('workspace');
workspace.appendChild(uiCanvas);
const resizeCanvas = () => {
uiCanvas.width = workspace.clientWidth;
uiCanvas.height = workspace.clientHeight;
};
// Initial resize
resizeCanvas();
// Resize canvas when window resizes
window.addEventListener('resize', resizeCanvas);
uiCanvas.addEventListener('mousedown', (event) => handleMouseEvent(event, 'mousedown'));
uiCanvas.addEventListener('mouseup', (event) => handleMouseEvent(event, 'mouseup'));
uiCanvas.addEventListener('mousemove', (event) => handleMouseEvent(event, 'mousemove'));
uiCanvas.addEventListener('contextmenu', (event) => event.preventDefault());
function handleMouseEvent(event, eventType) {
if (tool.eventHandlers) {
if ((event.buttons&4)==4) {
passEventToElementBelow(event);
}else {
if (tool.eventHandlers[eventType]) {
// Call the tool's event handler
tool.eventHandlers[eventType](event);
}
}
} else {
// Pass the event to the element below
passEventToElementBelow(event);
}
event.preventDefault();
}
function passEventToElementBelow(event) {
uiCanvas.style.pointerEvents = 'none';
// Find the element below the cursor
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
uiCanvas.style.pointerEvents = '';
// Dispatch the event to the element below
if (elemBelow) {
forwardEvent(event,elemBelow);
}
}
return uiCanvas;
}
function drawPicFramesOnUICanvas(uiCanvas, picList) {
const ctx = uiCanvas.getContext('2d');
ctx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); // Clear the canvas
picList.forEach(pic => {
// Convert pic's corners to UI Canvas coordinates
const topLeft = convertCoordsFromPicToUI(0, 0, pic);
const topRight = convertCoordsFromPicToUI(pic.width, 0, pic);
const bottomLeft = convertCoordsFromPicToUI(0, pic.height, pic);
const bottomRight = convertCoordsFromPicToUI(pic.width, pic.height, pic);
// Draw rectangle
ctx.beginPath();
ctx.moveTo(topLeft.x,topLeft.y);
ctx.lineTo(topRight.x,topRight.y);
ctx.lineTo(bottomRight.x,bottomRight.y);
ctx.lineTo(bottomLeft.x,bottomLeft.y);
ctx.lineTo(topLeft.x,topLeft.y);
ctx.strokeStyle = 'red';
ctx.stroke();
// Draw cross lines
ctx.beginPath();
ctx.moveTo(topLeft.x, topLeft.y);
ctx.lineTo(bottomRight.x, bottomRight.y);
ctx.moveTo(topRight.x, topRight.y);
ctx.lineTo(bottomLeft.x, bottomLeft.y);
ctx.stroke();
});
}
const pixelTip = {
@@ -24,6 +151,22 @@ const eyeDropper={
const color= "#" + toHex(sample[0]) + toHex(sample[1]) + toHex(sample[2]);
$("#foreground").val(color)
}
},
eventHandlers: {
mousedown(e) {
const {offsetX,offsetY} = e;
const pic = getDrawAreaAtPoint(offsetX,offsetY);
if (!pic) return;
const {x,y} = convertCoordsFromUIToPic(offsetX,offsetY,pic);
if (x>=0 && y>=0 && x<pic.width && y<pic.height) {
const sample = pic.canvas.ctx.getImageData(x,y,1,1).data;
const toHex = (byte) => byte.toString(16).padStart(2, '0');
const color= "#" + toHex(sample[0]) + toHex(sample[1]) + toHex(sample[2]);
$("#foreground").val(color)
}
console.log({x,y})
}
}
}
@@ -78,3 +221,252 @@ const pixelClear = {
}
const transformTool = (_=> { //closure
let rotateMode = false;
let doubleClickGap=300;
let mouseDownTime =0;
let mouseDownTransform = [1,0, 0,1, 0,0];
let mouseDownAngle = 0;
let layer;
let dragHandler;
let mouseDownPosition
let preserveAspect = false;
let scaleHandlers= [
dragTopLeft,dragTopRight,dragBottomRight,dragBottomLeft,dragTop,dragRight,dragBottom,dragLeft
];
const tool = {
init() {
this.drawUI();
},
drawOperation() {
console.log("transform tool should not draw, this is a bug")
},
drawUI() {
//drawPicFramesOnUICanvas(uiCanvas,[activePic]);
const ctx=uiCanvas.getContext("2d");
ctx.clearRect(0,0,1e5,1e5);
let handles=controlPoints(0,0,activePic.activeLayer.width,activePic.activeLayer.height);
ctx.fillStyle="black";
ctx.strokeStyle="white";
if (rotateMode) {
let {x,y} = activePic.activeLayer.rotationCenter;
const picPoints = activePic.activeLayer.convertToPicCoords(x,y)
const uiPos = convertCoordsFromPicToUI(picPoints.x,picPoints.y,activePic);
ctx.strokeStyle="black";
ctx.strokeRect(uiPos.x-5,uiPos.y-5,10,10);
ctx.strokeStyle="white";
ctx.strokeRect(uiPos.x-4,uiPos.y-4,8,8);
for (let{x,y} of handles) {
const picPoints = activePic.activeLayer.convertToPicCoords(x,y)
const uiPos = convertCoordsFromPicToUI(picPoints.x,picPoints.y,activePic);
ctx.beginPath();
ctx.arc(uiPos.x,uiPos.y,8,0,Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(uiPos.x,uiPos.y,7,0,Math.PI*2);
ctx.stroke();
}
} else {
for (let{x,y} of handles) {
const picPoints = activePic.activeLayer.convertToPicCoords(x,y)
const uiPos = convertCoordsFromPicToUI(picPoints.x,picPoints.y,activePic);
ctx.fillRect(uiPos.x-5,uiPos.y-5,10,10);
ctx.strokeRect(uiPos.x-4,uiPos.y-4,8,8);
}
}
}
}
tool.eventHandlers ={
mousedown(e) {
if (e.button == 0) {
let now = Date.now();
let clickSpacing = now-mouseDownTime;
mouseDownTime=now;
if (clickSpacing < doubleClickGap) {
doubleClickHandler(e);
return;
}
const mousePos = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
layer = activePic?.activeLayer;
if (!layer) return;
let handles=controlPoints(0,0,layer.width,layer.height);
dragHandler=null;
for (let i=0;i<handles.length;i++) {
const {x,y} = handles[i];
const picPoints = layer.convertToPicCoords(x,y)
//const uiPos = convertCoordsFromPicToUI(picPoints.x,picPoints.y,activePic);
//if (max( abs(uiPos.x-e.offsetX), abs(uiPos.y-e.offsetY) ) <8 ) {
if (max( abs(picPoints.x-mousePos.x), abs(picPoints.y-mousePos.y) ) <8 ) {
dragHandler=rotateMode?rotateHandler:scaleHandlers[i];
break;
}
}
if (!dragHandler) {
if ( mousePos.x >0 &&
mousePos.x<=layer.width &&
mousePos.y >0 &&
mousePos.y<=layer.height
) {
dragHandler=translateHandler;
}
if (rotateMode) {
let {x,y} = layer.convertToPicCoords(layer.rotationCenter.x,layer.rotationCenter.y);
console.log({x,y,mousePos})
if (max( abs(x-mousePos.x), abs(y-mousePos.y) ) <8 ) {
dragHandler=translateCenterHandler;
}
}
}
if (dragHandler) {
let center = layer.convertToPicCoords(layer.rotationCenter.x,layer.rotationCenter.y)
mouseDownAngle= v2Angle(v2Sub(mousePos,center));
mouseDownTransform=[...layer.transform];
mouseDownPosition = {x:e.offsetX,y:e.offsetY}
console.log({mouseDownAngle})
}
}
},
mouseup(e) {
},
mousemove(e) {
if ((e.buttons && 1) !== 1) dragStop();
if (dragHandler) dragHandler(e);
tool.drawUI();
},
}
function controlPoints (left, top, right, bottom) {
const midX = (left+right)/2;
const midY = (top+bottom)/2;
return [
{x:left, y:top},
{x:right, y:top},
{x:right, y:bottom},
{x:left, y:bottom},
{x:midX, y:top},
{x:right, y:midY},
{x:midX, y:bottom},
{x:left, y:midY}
]
}
function dragStop() {
dragHandler=null;
}
function doubleClickHandler(e) {
rotateMode=!rotateMode;
tool.drawUI();
}
function scaleHandler(handle,anchor,position,lockX=false,lockY=false) {
const newTransform=scaleTransformByHandle(handle,anchor,position,mouseDownTransform,lockX,lockY);
layer.transform = newTransform;
activePic.updateVisualRepresentation();
}
function translateHandler(e) {
let dx = e.offsetX -mouseDownPosition.x
let dy = e.offsetY -mouseDownPosition.y;
let newTransform = [...mouseDownTransform];
newTransform[4]+=dx;
newTransform[5]+=dy;
layer.transform=newTransform;
activePic.updateVisualRepresentation();
}
function translateCenterHandler(e) {
const mousePos = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
layer.rotationCenter= layer.convertFromPicCoords(mousePos.x,mousePos.y);
activePic.updateVisualRepresentation();
}
function dragTopLeft(e) {
const mouseCurrentPosition = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
scaleHandler({x:0,y:0},{x:layer.width,y:layer.height},mouseCurrentPosition);
}
function dragTopRight(e) {
const mouseCurrentPosition = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
scaleHandler({x:layer.width,y:0},{x:0,y:layer.height},mouseCurrentPosition);
}
function dragBottomRight(e) {
const mouseCurrentPosition = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
scaleHandler({x:layer.width,y:layer.height},{x:0,y:0},mouseCurrentPosition);
}
function dragBottomLeft(e) {
const mouseCurrentPosition = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
scaleHandler({x:0,y:layer.height},{x:layer.width,y:0},mouseCurrentPosition);
}
function dragTop(e) {
const mouseCurrentPosition = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
scaleHandler({x:layer.width/2,y:0},{x:layer.width,y:layer.height},mouseCurrentPosition,true);
}
function dragRight(e) {
const mouseCurrentPosition = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
scaleHandler({x:layer.width,y:layer.height/2},{x:0,y:0},mouseCurrentPosition,false,true);
}
function dragLeft(e) {
const mouseCurrentPosition = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
scaleHandler({x:0,y:layer.height/2},{x:layer.width,y:0},mouseCurrentPosition,false,true);
}
function dragBottom(e) {
const mouseCurrentPosition = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
scaleHandler({x:layer.width/2,y:layer.height},{x:layer.width,y:0},mouseCurrentPosition,true);
}
function rotateHandler(e) {
let center = layer.convertToPicCoords(layer.rotationCenter.x,layer.rotationCenter.y)
let mousePos = convertCoordsFromUIToPic(e.offsetX,e.offsetY,activePic);
let angle= v2Angle(v2Sub(mousePos,center));
let angleDelta = angle-mouseDownAngle;
layer.transform=[...mouseDownTransform];
layer.rotate(angleDelta*RadiansToDegrees);
activePic.updateVisualRepresentation();
}
function rotateTopRight(e) {
}
function rotateBottomLeft(e) {
}
function rotateBottomRight(e) {
}
function rotateTop(e) {
}
function rotateBottom(e) {
}
function rotateLeft(e) {
}
function rotateRight(e) {
}
return tool;
})(); //end closure for transformTool
+6
View File
@@ -94,6 +94,12 @@ svg.tool.button {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath d='M34 10A1 1 90 0139 15L34 19 33 20 34 21 33 22 27 16 28 15 29 16 30 15ZM27.4 17.4 12.1 31.4 12.1 33.3 9.3 35.7 9.5 36.9 13.2 33.7 13.2 31.7 27.5 18.9 30.2 21.5 16.1 34.4 14.1 34.6 10.5 37.7 12 37.9 14.6 35.5 16.4 35.4 31.5 21.7'/%3E%3C/svg%3E");
}
#transform.tool.button {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-2 -2 32 32'%3E%3Cpath d='m14 2 4 4h-3v7h7v-3l4 4-4 3v-2h-7v7h3l-4 4-4-4h3v-7h-7v3l-4-4 4-4v3h7v-7h-3z'/%3E%3C/svg%3E");
}
#clear.action.button {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath d='M19 24 12 17 17 12 24 19 31 12 36 17 29 24 36 31 31 36 24 29 17 36 12 31'/%3E%3C/svg%3E");
}
+241
View File
@@ -1,5 +1,191 @@
// these are support functions for bspaint,
// Nothing in here should rely on global state
'use strict';
const abs=Math.abs;
const min=Math.min;
const max=Math.max;
const median = (a, b, c) => max(min(a, b), min(max(a, b), c));
const RadiansToDegrees = 180/Math.PI;
function v2Distance(a,b) {
return Math.hypot(a.x-b.x,a.y-b.y);
}
function v2Length({x,y}) {
return Math.hypot(x,y)
}
function v2Sub(a,b) {
return {x:a.x-b.x,y:a.y-b.y};
}
function v2Add(a,b) {
return {x:a.x+b.x,y:a.y+b.y};
}
function v2Scale(a,scale) {
return {x:a.x*scale,y:a.y*scale}
}
function v2Normalise(a,length=1) {
const scale = length/v2Length(a);
return v2Scale(a,scale);
}
function v2Dot(a,b) {
return (a.x*b.x + a.y*b.y);
}
function v2PurpCW({x, y}) {
return {x: y, y: -x};
}
function v2PurpCCW({x, y}) {
return {x: -y, y: x};
}
function v2Angle({x,y}) {
return Math.atan2(y,x);
}
var v2Purp=v2PurpCCW;
function applyTransform({x,y},transform) {
const [a, b, c, d, e, f] = transform;
// Apply the layer's transformation matrix
const newX = a * x + c * y + e;
const newY = b * x + d * y + f;
return { x: newX, y: newY };
}
function reverseTransform({x,y},transform) {
const [a, b, c, d, e, f] = transform;
// Apply the inverse of the layer's transformation matrix
const det = a * d - b * c;
if (det === 0) {
console.error("Transformation matrix is not invertible");
return {x,y}
}
const newX = (d * (x - e) - c * (y - f)) / det;
const newY = (-b * (x - e) + a * (y - f)) / det;
return { x: newX, y: newY };
}
function concatenateTransforms([a1, b1, c1, d1, e1, f1], [a2, b2, c2, d2, e2, f2]) {
return [
a1 * a2 + c1 * b2,
b1 * a2 + d1 * b2,
a1 * c2 + c1 * d2,
b1 * c2 + d1 * d2,
a1 * e2 + c1 * f2 + e1,
b1 * e2 + d1 * f2 + f1
];
}
function scaleTranformWithSkewFudge(A, B, newTransformedA, originalTransform, skewCompensation=0, lockX=false,lockY=false) {
const transformedA = applyTransform(A, originalTransform);
const transformedB = applyTransform(B, originalTransform);
// Calculate relative differences
const originalRelDiff = {
x: transformedB.x - transformedA.x,
y: transformedB.y - transformedA.y
};
const newRelDiff = {
x: transformedB.x - newTransformedA.x,
y: transformedB.y - newTransformedA.y
};
const angle = Math.atan2(originalTransform[1], originalTransform[0]);
// Calculate inverse rotation matrix
const cosTheta = Math.cos(-angle);
const sinTheta = Math.sin(-angle);
const inverseRotation = [cosTheta, sinTheta, -sinTheta, cosTheta, 0, 0];
// Apply inverse rotation to the relative differences
const rotatedOriginalRelDiff = applyTransform(originalRelDiff, inverseRotation);
const rotatedNewRelDiff = applyTransform(newRelDiff, inverseRotation);
// Calculate scale factors
let scale = {
x: rotatedNewRelDiff.x / rotatedOriginalRelDiff.x,
y: rotatedNewRelDiff.y / rotatedOriginalRelDiff.y
}
const divergence =v2Dot( v2Normalise(v2Sub(transformedB,transformedA)), v2Purp(v2Normalise(v2Sub(transformedA,newTransformedA))));
const lengthAMovement = v2Distance(transformedA,newTransformedA);
const scaleTransform= [scale.x + skewCompensation* divergence*lengthAMovement, 0, 0, scale.y, 0, 0];
if (lockX) scaleTransform[0]=1;
if (lockY) scaleTransform[3]=1;
const newTransform = concatenateTransforms(originalTransform,scaleTransform);
const newTransformedB = applyTransform(B,newTransform);
// Calculate translation to keep B in place
const translateX = transformedB.x - newTransformedB.x;
const translateY = transformedB.y - newTransformedB.y;
const translation = [1, 0, 0, 1, translateX, translateY];
const translated = concatenateTransforms(translation,originalTransform);
const result = concatenateTransforms(translated,scaleTransform);
return result;
}
function scaleTransformWithDynamicFix(a,b,desiredA,transform,lockX = false,lockY=false) {
function errorVector(skewCandidate) {
const newTransform = scaleTranformWithSkewFudge(a, b, desiredA, transform,skewCandidate,false,false);
const actualA = applyTransform(a,newTransform);
return v2Sub(actualA,desiredA);
}
const candidate1 = 0;
const candidate2 = 110;
var skewAdjustment=0;
let errorVector1 = errorVector(candidate1);
if (errorVector1.x===0 && errorVector1.y===0) {
//All good to go
skewAdjustment=0;
} else {
let errorVector2 = errorVector(candidate2);
// Calculate the slopes for x and y components of the error vector
let slopeX = (errorVector2.x - errorVector1.x) / (candidate2 - candidate1);
let slopeY = (errorVector2.y - errorVector1.y) / (candidate2 - candidate1);
// Calculate where the line crosses zero for both components
let zeroCrossingX = candidate1 - errorVector1.x / slopeX;
let zeroCrossingY = candidate1 - errorVector1.y / slopeY;
if (slopeY==0) {
skewAdjustment=slopeX;
}
else if (slopeX == 0) {
skewAdjustment=slopeY;
} else {
//not strictly necessary to average here but we're in full-on dodgy math country now.
skewAdjustment = (zeroCrossingX + zeroCrossingY) / 2;
}
}
return scaleTranformWithSkewFudge(a,b,desiredA,transform,skewAdjustment,lockX,lockY)
}
function scaleTransformByHandle(a, stationaryPoint, desiredTransformedA, originalTransform, lockX = false, lockY=false) {
return scaleTransformWithDynamicFix(a,stationaryPoint,desiredTransformedA,originalTransform,lockX,lockY)
}
function cells(x=1,y=1) {
let width = 1/x;
@@ -172,3 +358,58 @@ function circleBrush(scale) {
if ((newCursor.width < 2) || (newCursor.width>120)) value="crosshair";
return value;
}
function cloneEvent(event, modifications = {}) {
let eventProperties = {
"bubbles": event.bubbles,
"cancelBubble": event.cancelBubble,
"cancelable": event.cancelable,
"composed": event.composed,
"currentTarget": event.currentTarget,
"defaultPrevented": event.defaultPrevented,
"eventPhase": event.eventPhase,
"isTrusted": event.isTrusted,
"target": event.target,
"timeStamp": event.timeStamp,
"type": event.type,
};
// Checking if the event is a MouseEvent, add mouse event properties
if(event instanceof MouseEvent) {
eventProperties = {
...eventProperties,
"altKey": event.altKey,
"button": event.button,
"buttons": event.buttons,
"clientX": event.clientX,
"clientY": event.clientY,
"ctrlKey": event.ctrlKey,
"metaKey": event.metaKey,
"movementX": event.movementX,
"movementY": event.movementY,
"offsetX": event.offsetX,
"offsetY": event.offsetY,
"pageX": event.pageX,
"pageY": event.pageY,
"relatedTarget": event.relatedTarget,
"screenX": event.screenX,
"screenY": event.screenY,
"shiftKey": event.shiftKey,
};
}
return { ...modifications, ...eventProperties };
}
function forwardEvent(mouseEvent, recipient) {
var rect = recipient.getBoundingClientRect();
const options = cloneEvent(mouseEvent,{
clientX:mouseEvent.clientX - rect.left,
clientY:mouseEvent.clientY - rect.top,
});
var newEvent = new MouseEvent(mouseEvent.type, options )
recipient.dispatchEvent(newEvent);
}