commit
ec4054529c
16 changed files with 983 additions and 477 deletions
|
@ -17,7 +17,9 @@
|
|||
bottom: 15%;
|
||||
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
|
||||
max-height: 70%;
|
||||
aspect-ratio: 1;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
.layer-render-target .collection {
|
||||
position: absolute;
|
||||
transform-origin: 0px 0px;
|
||||
}
|
||||
|
||||
.layer-render-target .collection > .collection-input-overlay {
|
||||
|
@ -45,7 +46,7 @@
|
|||
right: 0;
|
||||
}
|
||||
|
||||
#layer-overlay {
|
||||
.overlay-canvas {
|
||||
position: fixed;
|
||||
|
||||
top: 0;
|
||||
|
|
|
@ -231,6 +231,7 @@ div.autocomplete > .autocomplete-list {
|
|||
}
|
||||
|
||||
div.autocomplete > .autocomplete-list > .autocomplete-option {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
|
|
|
@ -158,7 +158,9 @@
|
|||
|
||||
background-color: #293d3d77;
|
||||
|
||||
-webkit-mask-image: url("../../res/icons/chevron-up.svg");
|
||||
mask-image: url("../../res/icons/chevron-up.svg");
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
|
||||
width: 60px;
|
||||
|
|
32
index.html
32
index.html
|
@ -5,15 +5,15 @@
|
|||
<title>openOutpaint 🐠</title>
|
||||
<!-- CSS Variables -->
|
||||
<link href="css/colors.css?v=3f81e80" rel="stylesheet" />
|
||||
<link href="css/icons.css?v=caa702e" rel="stylesheet" />
|
||||
<link href="css/icons.css?v=9ae0466" rel="stylesheet" />
|
||||
|
||||
<link href="css/index.css?v=5b8d4d6" rel="stylesheet" />
|
||||
<link href="css/layers.css?v=b4fbf61" rel="stylesheet" />
|
||||
<link href="css/layers.css?v=92c0352" rel="stylesheet" />
|
||||
|
||||
<link href="css/ui/generic.css?v=4b9afe2" rel="stylesheet" />
|
||||
<link href="css/ui/generic.css?v=802bd41" rel="stylesheet" />
|
||||
|
||||
<link href="css/ui/history.css?v=0b03861" rel="stylesheet" />
|
||||
<link href="css/ui/layers.css?v=4fd95fe" rel="stylesheet" />
|
||||
<link href="css/ui/layers.css?v=ae472cd" rel="stylesheet" />
|
||||
<link href="css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
|
||||
|
||||
<!-- Tool Specific CSS -->
|
||||
|
@ -49,6 +49,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- Prompts section -->
|
||||
<button type="button" class="collapsible">Prompts</button>
|
||||
<div class="content prompt">
|
||||
|
@ -319,7 +320,8 @@
|
|||
<div id="layer-render" class="layer-render-target"></div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<canvas id="layer-overlay" class="layer-overlay"></canvas>
|
||||
<canvas id="layer-overlay" class="overlay-canvas"></canvas>
|
||||
<canvas id="layer-debug-overlay" class="overlay-canvas"></canvas>
|
||||
|
||||
<!-- Page Overlay -->
|
||||
<div id="page-overlay-wrapper" class="page-overlay invisible">
|
||||
|
@ -331,7 +333,7 @@
|
|||
<div class="ui separator"></div>
|
||||
<iframe
|
||||
id="page-overlay"
|
||||
src="pages/configuration.html?v=3d710ce"></iframe>
|
||||
src="pages/configuration.html?v=7fca00b"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -339,21 +341,21 @@
|
|||
<script src="js/global.js?v=3a1cde6" type="text/javascript"></script>
|
||||
|
||||
<!-- Base Libs -->
|
||||
<script src="js/lib/util.js?v=7f6847c" type="text/javascript"></script>
|
||||
<script src="js/lib/util.js?v=49a78a6" type="text/javascript"></script>
|
||||
<script src="js/lib/events.js?v=2ab7933" type="text/javascript"></script>
|
||||
<script src="js/lib/input.js?v=09298ac" type="text/javascript"></script>
|
||||
<script src="js/lib/layers.js?v=a1f8aea" type="text/javascript"></script>
|
||||
<script src="js/lib/commands.js?v=bf23c83" type="text/javascript"></script>
|
||||
|
||||
<script src="js/lib/toolbar.js?v=ca3fccf" type="text/javascript"></script>
|
||||
<script src="js/lib/toolbar.js?v=306d637" type="text/javascript"></script>
|
||||
<script src="js/lib/ui.js?v=76ede2b" type="text/javascript"></script>
|
||||
|
||||
<script
|
||||
src="js/initalize/layers.populate.js?v=c81f0a5"
|
||||
src="js/initalize/layers.populate.js?v=39785ac"
|
||||
type="text/javascript"></script>
|
||||
|
||||
<!-- Configuration -->
|
||||
<script src="js/config.js?v=8da6a43" type="text/javascript"></script>
|
||||
<script src="js/config.js?v=e0345e0" type="text/javascript"></script>
|
||||
|
||||
<!-- Content -->
|
||||
<script src="js/prompt.js?v=7a1c68c" type="text/javascript"></script>
|
||||
|
@ -368,20 +370,20 @@
|
|||
|
||||
<!-- Load Tools -->
|
||||
<script
|
||||
src="js/ui/tool/generic.js?v=2bcd36d"
|
||||
src="js/ui/tool/generic.js?v=3e678e0"
|
||||
type="text/javascript"></script>
|
||||
|
||||
<script src="js/ui/tool/dream.js?v=f18c203" type="text/javascript"></script>
|
||||
<script src="js/ui/tool/dream.js?v=eb98dc9" type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/tool/maskbrush.js?v=1e8a893"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/tool/colorbrush.js?v=8acb4f6"
|
||||
src="js/ui/tool/colorbrush.js?v=3f8c01a"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/tool/select.js?v=ade791e"
|
||||
src="js/ui/tool/select.js?v=e27bbdf"
|
||||
type="text/javascript"></script>
|
||||
<script src="js/ui/tool/stamp.js?v=3c07ac8" type="text/javascript"></script>
|
||||
<script src="js/ui/tool/stamp.js?v=4a86ff8" type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/tool/interrogate.js?v=e579ff1"
|
||||
type="text/javascript"></script>
|
||||
|
|
28
js/config.js
28
js/config.js
|
@ -5,9 +5,37 @@
|
|||
*/
|
||||
const config = makeReadOnly(
|
||||
{
|
||||
// Grid Size
|
||||
gridSize: 64,
|
||||
|
||||
// Scroll Tick Limit (How much must scroll to reach next tick)
|
||||
wheelTickSize: 50,
|
||||
|
||||
/** Select Tool */
|
||||
// Handle Draw Size
|
||||
handleDrawSize: 12,
|
||||
// Handle Draw Hover Scale
|
||||
handleDrawHoverScale: 1.3,
|
||||
// Handle Detect Size
|
||||
handleDetectSize: 20,
|
||||
// Rotate Handle Distance (from selection)
|
||||
rotateHandleDistance: 32,
|
||||
|
||||
// Rotation Snapping Distance
|
||||
rotationSnappingDistance: (10 * Math.PI) / 180,
|
||||
// Rotation Snapping Angles
|
||||
rotationSnappingAngles: [
|
||||
(-Math.PI * 4) / 4,
|
||||
(-Math.PI * 3) / 4,
|
||||
(-Math.PI * 2) / 4,
|
||||
(-Math.PI * 1) / 4,
|
||||
0,
|
||||
(Math.PI * 1) / 4,
|
||||
(Math.PI * 2) / 4,
|
||||
(Math.PI * 3) / 4,
|
||||
(Math.PI * 4) / 4,
|
||||
],
|
||||
|
||||
// Endpoint
|
||||
api: makeReadOnly({path: "/sdapi/v1/"}),
|
||||
},
|
||||
|
|
|
@ -160,61 +160,72 @@ debugLayer.hide(); // Hidden by default
|
|||
* The global viewport object (may be modularized in the future). All
|
||||
* coordinates given are of the center of the viewport
|
||||
*
|
||||
* cx and cy are the viewport's world coordinates, scaled to zoom level.
|
||||
* _x and _y are actual coordinates in the DOM space
|
||||
* cx and cy are the viewport's world coordinates.
|
||||
*
|
||||
* The transform() function does some transforms and writes them to the
|
||||
* provided element.
|
||||
*/
|
||||
const viewport = {
|
||||
get cx() {
|
||||
return this._x * this.zoom;
|
||||
},
|
||||
class Viewport {
|
||||
cx = 0;
|
||||
cy = 0;
|
||||
|
||||
set cx(v) {
|
||||
return (this._x = v / this.zoom);
|
||||
},
|
||||
_x: 0,
|
||||
get cy() {
|
||||
return this._y * this.zoom;
|
||||
},
|
||||
set cy(v) {
|
||||
return (this._y = v / this.zoom);
|
||||
},
|
||||
_y: 0,
|
||||
zoom: 1,
|
||||
rotation: 0,
|
||||
zoom = 1;
|
||||
|
||||
/**
|
||||
* Gets viewport width in canvas coordinates
|
||||
*/
|
||||
get w() {
|
||||
return (window.innerWidth * 1) / this.zoom;
|
||||
},
|
||||
return window.innerWidth * this.zoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets viewport height in canvas coordinates
|
||||
*/
|
||||
get h() {
|
||||
return (window.innerHeight * 1) / this.zoom;
|
||||
},
|
||||
return window.innerHeight * this.zoom;
|
||||
}
|
||||
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
get v2c() {
|
||||
const m = new DOMMatrix();
|
||||
|
||||
m.translateSelf(-this.w / 2, -this.h / 2);
|
||||
m.translateSelf(this.cx, this.cy);
|
||||
m.scaleSelf(this.zoom);
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
get c2v() {
|
||||
return this.v2c.invertSelf();
|
||||
}
|
||||
|
||||
viewToCanvas(x, y) {
|
||||
return {
|
||||
x: this.cx + this.w * (x / window.innerWidth - 0.5),
|
||||
y: this.cy + this.h * (y / window.innerHeight - 0.5),
|
||||
};
|
||||
},
|
||||
if (x.x !== undefined) return this.v2c.transformPoint(x);
|
||||
return this.v2c.transformPoint({x, y});
|
||||
}
|
||||
|
||||
canvasToView(x, y) {
|
||||
return {
|
||||
x: window.innerWidth * ((x - this.cx) / this.w) + window.innerWidth / 2,
|
||||
y: window.innerHeight * ((y - this.cy) / this.h) + window.innerHeight / 2,
|
||||
};
|
||||
},
|
||||
if (x.x !== undefined) return this.c2v.transformPoint(x);
|
||||
return this.c2v.transformPoint({x, y});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply transformation
|
||||
*
|
||||
* @param {HTMLElement} el Element to apply CSS transform to
|
||||
*/
|
||||
transform(el) {
|
||||
el.style.transformOrigin = `${this.cx}px ${this.cy}px`;
|
||||
el.style.transform = `scale(${this.zoom}) translate(${-(
|
||||
this._x -
|
||||
this.w / 2
|
||||
)}px, ${-(this._y - this.h / 2)}px)`;
|
||||
},
|
||||
};
|
||||
el.style.transformOrigin = "0px 0px";
|
||||
el.style.transform = this.c2v;
|
||||
}
|
||||
}
|
||||
|
||||
const viewport = new Viewport(0, 0);
|
||||
|
||||
viewport.cx = imageCollection.size.w / 2;
|
||||
viewport.cy = imageCollection.size.h / 2;
|
||||
|
@ -296,7 +307,7 @@ mouse.listen.camera.onwheel.on((evn) => {
|
|||
const pcy = viewport.cy;
|
||||
|
||||
// Apply zoom
|
||||
viewport.zoom *= 1 - evn.delta * 0.0002;
|
||||
viewport.zoom *= 1 + evn.delta * 0.0002;
|
||||
|
||||
// Apply normal zoom (center of viewport)
|
||||
viewport.cx = pcx;
|
||||
|
@ -305,13 +316,13 @@ mouse.listen.camera.onwheel.on((evn) => {
|
|||
viewport.transform(imageCollection.element);
|
||||
|
||||
// Calculate new viewport center and move
|
||||
const newCursorPosition = viewport.viewToCanvas(evn.x, evn.y);
|
||||
viewport.cx = pcx - (newCursorPosition.x - cursorPosition.x);
|
||||
viewport.cy = pcy - (newCursorPosition.y - cursorPosition.y);
|
||||
//const newCursorPosition = viewport.viewToCanvas(evn.x, evn.y);
|
||||
//viewport.cx = pcx - (newCursorPosition.x - cursorPosition.x);
|
||||
//viewport.cy = pcy - (newCursorPosition.y - cursorPosition.y);
|
||||
|
||||
viewport.transform(imageCollection.element);
|
||||
//viewport.transform(imageCollection.element);
|
||||
|
||||
toolbar.currentTool.redraw();
|
||||
toolbar._current_tool.redrawui && toolbar._current_tool.redrawui();
|
||||
});
|
||||
|
||||
const cameraPaintStart = (evn) => {
|
||||
|
@ -320,8 +331,8 @@ const cameraPaintStart = (evn) => {
|
|||
|
||||
const cameraPaint = (evn) => {
|
||||
if (worldInit) {
|
||||
viewport.cx = worldInit.x + (evn.ix - evn.x) / viewport.zoom;
|
||||
viewport.cy = worldInit.y + (evn.iy - evn.y) / viewport.zoom;
|
||||
viewport.cx = worldInit.x + (evn.ix - evn.x) * viewport.zoom;
|
||||
viewport.cy = worldInit.y + (evn.iy - evn.y) * viewport.zoom;
|
||||
|
||||
// Limits
|
||||
viewport.cx = Math.max(
|
||||
|
@ -337,6 +348,9 @@ const cameraPaint = (evn) => {
|
|||
}
|
||||
|
||||
viewport.transform(imageCollection.element);
|
||||
toolbar._current_tool.state.redrawui &&
|
||||
toolbar._current_tool.state.redrawui();
|
||||
|
||||
if (global.debug) {
|
||||
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
|
||||
debugCtx.fillStyle = "#F0F";
|
||||
|
|
|
@ -90,14 +90,20 @@ const toolbar = {
|
|||
name: toolname,
|
||||
enabled: false,
|
||||
_element: null,
|
||||
state: {},
|
||||
state: {
|
||||
redrawui: () => tool.state.redraw && tool.state.redraw(),
|
||||
},
|
||||
options,
|
||||
/**
|
||||
* If the tool has a redraw() function in its state, then run it
|
||||
*/
|
||||
redraw: () => {
|
||||
tool.state.redrawui && tool.state.redrawui();
|
||||
tool.state.redraw && tool.state.redraw();
|
||||
},
|
||||
redrawui: () => {
|
||||
tool.state.redrawui && tool.state.redrawui();
|
||||
},
|
||||
enable: (opt = null) => {
|
||||
if (toolbar._locked) return;
|
||||
|
||||
|
|
|
@ -32,6 +32,31 @@ class BoundingBox {
|
|||
w = 0;
|
||||
h = 0;
|
||||
|
||||
/** @type {Point} */
|
||||
get tl() {
|
||||
return {x: this.x, y: this.y};
|
||||
}
|
||||
|
||||
/** @type {Point} */
|
||||
get tr() {
|
||||
return {x: this.x + this.w, y: this.y};
|
||||
}
|
||||
|
||||
/** @type {Point} */
|
||||
get bl() {
|
||||
return {x: this.x, y: this.y + this.h};
|
||||
}
|
||||
|
||||
/** @type {Point} */
|
||||
get br() {
|
||||
return {x: this.x + this.w, y: this.y + this.h};
|
||||
}
|
||||
|
||||
/** @type {Point} */
|
||||
get center() {
|
||||
return {x: this.x + this.w / 2, y: this.y + this.h / 2};
|
||||
}
|
||||
|
||||
constructor({x, y, w, h} = {x: 0, y: 0, w: 0, h: 0}) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
@ -64,6 +89,18 @@ class BoundingBox {
|
|||
h: maxy - miny,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a transformed bounding box (using top-left, bottom-right points)
|
||||
*
|
||||
* @param {DOMMatrix} transform Transformation matrix to transform points
|
||||
*/
|
||||
transform(transform) {
|
||||
return BoundingBox.fromStartEnd(
|
||||
transform.transformPoint({x: this.x, y: this.y}),
|
||||
transform.transformPoint({x: this.x + this.w, y: this.y + this.h})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -176,7 +176,7 @@ const colorBrushTool = () =>
|
|||
uiCtx.arc(
|
||||
vcp.x,
|
||||
vcp.y,
|
||||
(state.eyedropper ? 50 : state.brushSize / 2) * viewport.zoom,
|
||||
(state.eyedropper ? 50 : state.brushSize / 2) / viewport.zoom,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
true
|
||||
|
@ -197,7 +197,7 @@ const colorBrushTool = () =>
|
|||
uiCtx.arc(
|
||||
vcp.x,
|
||||
vcp.y,
|
||||
(state.brushSize / 2) * viewport.zoom,
|
||||
state.brushSize / (2 * viewport.zoom),
|
||||
0,
|
||||
2 * Math.PI,
|
||||
true
|
||||
|
|
|
@ -1241,10 +1241,16 @@ const _dream_onwheel = (evn, state) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// A simple but (I hope) effective fix for mouse wheel behavior
|
||||
_dream_wheel_accum += evn.delta;
|
||||
let delta = evn.delta;
|
||||
if (evn.evn.shiftKey) delta *= 0.01;
|
||||
|
||||
if (Math.abs(_dream_wheel_accum) > config.wheelTickSize) {
|
||||
// A simple but (I hope) effective fix for mouse wheel behavior
|
||||
_dream_wheel_accum += delta;
|
||||
|
||||
if (
|
||||
!evn.evn.shiftKey &&
|
||||
Math.abs(_dream_wheel_accum) > config.wheelTickSize
|
||||
) {
|
||||
// Snap to next or previous position
|
||||
const v =
|
||||
state.cursorSize -
|
||||
|
@ -1253,6 +1259,12 @@ const _dream_onwheel = (evn, state) => {
|
|||
state.cursorSize = state.setCursorSize(v + snap(v, 0, 128));
|
||||
state.mousemovecb(evn);
|
||||
|
||||
_dream_wheel_accum = 0; // Zero accumulation
|
||||
} else if (evn.evn.shiftKey && Math.abs(_dream_wheel_accum) >= 1) {
|
||||
const v = state.cursorSize - _dream_wheel_accum;
|
||||
state.cursorSize = state.setCursorSize(v);
|
||||
state.mousemovecb(evn);
|
||||
|
||||
_dream_wheel_accum = 0; // Zero accumulation
|
||||
}
|
||||
};
|
||||
|
@ -1942,11 +1954,10 @@ const img2imgTool = () =>
|
|||
return;
|
||||
}
|
||||
|
||||
const bbvp = {
|
||||
...viewport.canvasToView(bb.x, bb.y),
|
||||
w: viewport.zoom * bb.w,
|
||||
h: viewport.zoom * bb.h,
|
||||
};
|
||||
const bbvp = BoundingBox.fromStartEnd(
|
||||
viewport.canvasToView(bb.tl),
|
||||
viewport.canvasToView(bb.br)
|
||||
);
|
||||
|
||||
// For displaying border mask
|
||||
const bbCanvas = document.createElement("canvas");
|
||||
|
|
|
@ -27,11 +27,7 @@ const _tool = {
|
|||
reticleStyle: global.hasActiveInput ? "#BBF" : "#FFF",
|
||||
});
|
||||
|
||||
const bbvp = {
|
||||
...viewport.canvasToView(bb.x, bb.y),
|
||||
w: viewport.zoom * bb.w,
|
||||
h: viewport.zoom * bb.h,
|
||||
};
|
||||
const bbvp = bb.transform(viewport.c2v);
|
||||
|
||||
uiCtx.save();
|
||||
|
||||
|
@ -174,6 +170,8 @@ const _tool = {
|
|||
|
||||
/**
|
||||
* Gets the selection bounding box
|
||||
*
|
||||
* @returns {BoundingBox}
|
||||
*/
|
||||
get bb() {
|
||||
if (this._dirty_bb && this._selected) {
|
||||
|
@ -273,4 +271,389 @@ const _tool = {
|
|||
|
||||
return selection;
|
||||
},
|
||||
|
||||
/**
|
||||
* Processes cursor position
|
||||
*
|
||||
* @param {Point} wpoint World coordinate of the cursor
|
||||
* @param {boolean} snapToGrid Snap to grid
|
||||
*/
|
||||
_process_cursor(wpoint, snapToGrid) {
|
||||
// Get cursor positions
|
||||
let x = wpoint.x;
|
||||
let y = wpoint.y;
|
||||
let sx = x;
|
||||
let sy = y;
|
||||
|
||||
if (snapToGrid) {
|
||||
sx += snap(x, 0, config.gridSize);
|
||||
sy += snap(y, 0, config.gridSize);
|
||||
}
|
||||
|
||||
const vpc = viewport.canvasToView(x, y);
|
||||
const vpsc = viewport.canvasToView(sx, sy);
|
||||
|
||||
return {
|
||||
// World Coordinates
|
||||
x,
|
||||
y,
|
||||
sx,
|
||||
sy,
|
||||
|
||||
// Viewport Coordinates
|
||||
vpx: vpc.x,
|
||||
vpy: vpc.y,
|
||||
vpsx: vpsc.x,
|
||||
vpsy: vpsc.y,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Represents a marquee selection with an image
|
||||
*/
|
||||
MarqueeSelection: class {
|
||||
/** @type {HTMLCanvasElement} */
|
||||
canvas;
|
||||
|
||||
_dirty = false;
|
||||
_position = {x: 0, y: 0};
|
||||
/**
|
||||
* @type {Point}
|
||||
*/
|
||||
get position() {
|
||||
return this._position;
|
||||
}
|
||||
set position(v) {
|
||||
this._dirty = true;
|
||||
this._position = v;
|
||||
}
|
||||
|
||||
_scale = {x: 1, y: 1};
|
||||
/**
|
||||
* @type {Point}
|
||||
*/
|
||||
get scale() {
|
||||
return this._scale;
|
||||
}
|
||||
set scale(v) {
|
||||
if (v.x === 0 || v.y === 0) return;
|
||||
this._dirty = true;
|
||||
this._scale = v;
|
||||
}
|
||||
|
||||
_rotation = 0;
|
||||
get rotation() {
|
||||
return this._rotation;
|
||||
}
|
||||
set rotation(v) {
|
||||
this._dirty = true;
|
||||
this._rotation = v;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLCanvasElement} canvas Selected image canvas
|
||||
* @param {Point} position Initial position of the selection
|
||||
*/
|
||||
constructor(canvas, position = {x: 0, y: 0}) {
|
||||
this.canvas = canvas;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
/** @type {DOMMatrix} */
|
||||
_rtmatrix = null;
|
||||
get rtmatrix() {
|
||||
if (!this._rtmatrix || this._dirty) {
|
||||
const m = new DOMMatrix();
|
||||
|
||||
m.translateSelf(this.position.x, this.position.y);
|
||||
m.rotateSelf((this.rotation * 180) / Math.PI);
|
||||
|
||||
this._rtmatrix = m;
|
||||
}
|
||||
|
||||
return this._rtmatrix;
|
||||
}
|
||||
|
||||
/** @type {DOMMatrix} */
|
||||
_matrix = null;
|
||||
get matrix() {
|
||||
if (!this._matrix || this._dirty) {
|
||||
this._matrix = this.rtmatrix.scaleSelf(this.scale.x, this.scale.y);
|
||||
}
|
||||
return this._matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the main marquee box contains a given point
|
||||
*
|
||||
* @param {number} x X coordinate of the point
|
||||
* @param {number} y Y coordinate of the point
|
||||
*/
|
||||
contains(x, y) {
|
||||
const p = this.matrix.invertSelf().transformPoint({x, y});
|
||||
|
||||
return (
|
||||
Math.abs(p.x) < this.canvas.width / 2 &&
|
||||
Math.abs(p.y) < this.canvas.height / 2
|
||||
);
|
||||
}
|
||||
|
||||
hoveringRotateHandle(x, y, scale = 1) {
|
||||
const localc = this.rtmatrix.inverse().transformPoint({x, y});
|
||||
const localrh = {
|
||||
x: 0,
|
||||
y:
|
||||
(-this.scale.y * this.canvas.height) / 2 -
|
||||
config.rotateHandleDistance * scale,
|
||||
};
|
||||
|
||||
const dx = Math.abs(localc.x - localrh.x);
|
||||
const dy = Math.abs(localc.y - localrh.y);
|
||||
|
||||
return (
|
||||
dx * dx + dy * dy <
|
||||
(scale * scale * config.handleDetectSize * config.handleDetectSize) / 4
|
||||
);
|
||||
}
|
||||
|
||||
hoveringHandle(x, y, scale = 1) {
|
||||
const localbb = new BoundingBox({
|
||||
x: (this.scale.x * -this.canvas.width) / 2,
|
||||
y: (this.scale.y * -this.canvas.height) / 2,
|
||||
w: this.canvas.width * this.scale.x,
|
||||
h: this.canvas.height * this.scale.y,
|
||||
});
|
||||
|
||||
const localc = this.rtmatrix.inverse().transformPoint({x, y});
|
||||
const ontl =
|
||||
Math.max(
|
||||
Math.abs(localc.x - localbb.tl.x),
|
||||
Math.abs(localc.y - localbb.tl.y)
|
||||
) <
|
||||
(config.handleDetectSize / 2) * scale;
|
||||
const ontr =
|
||||
Math.max(
|
||||
Math.abs(localc.x - localbb.tr.x),
|
||||
Math.abs(localc.y - localbb.tr.y)
|
||||
) <
|
||||
(config.handleDetectSize / 2) * scale;
|
||||
const onbl =
|
||||
Math.max(
|
||||
Math.abs(localc.x - localbb.bl.x),
|
||||
Math.abs(localc.y - localbb.bl.y)
|
||||
) <
|
||||
(config.handleDetectSize / 2) * scale;
|
||||
const onbr =
|
||||
Math.max(
|
||||
Math.abs(localc.x - localbb.br.x),
|
||||
Math.abs(localc.y - localbb.br.y)
|
||||
) <
|
||||
(config.handleDetectSize / 2) * scale;
|
||||
|
||||
return {onHandle: ontl || ontr || onbl || onbr, ontl, ontr, onbl, onbr};
|
||||
}
|
||||
|
||||
hoveringBox(x, y) {
|
||||
const localbb = new BoundingBox({
|
||||
x: -this.canvas.width / 2,
|
||||
y: -this.canvas.height / 2,
|
||||
w: this.canvas.width,
|
||||
h: this.canvas.height,
|
||||
});
|
||||
|
||||
const localc = this.matrix.inverse().transformPoint({x, y});
|
||||
|
||||
return (
|
||||
!this.hoveringHandle(x, y).onHandle &&
|
||||
localbb.contains(localc.x, localc.y)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the marquee selector box
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} context A context for rendering the box to
|
||||
* @param {Point} cursor Cursor position
|
||||
* @param {DOMMatrix} transform A transformation matrix to transform the position by
|
||||
*/
|
||||
drawBox(context, cursor, transform = new DOMMatrix()) {
|
||||
const drawscale =
|
||||
1 / Math.sqrt(transform.a * transform.a + transform.b * transform.b);
|
||||
const m = transform.multiply(this.matrix);
|
||||
|
||||
context.save();
|
||||
|
||||
const localbb = new BoundingBox({
|
||||
x: -this.canvas.width / 2,
|
||||
y: -this.canvas.height / 2,
|
||||
w: this.canvas.width,
|
||||
h: this.canvas.height,
|
||||
});
|
||||
|
||||
// Line Style
|
||||
context.strokeStyle = "#FFF";
|
||||
context.lineWidth = 2;
|
||||
|
||||
const tl = m.transformPoint(localbb.tl);
|
||||
const tr = m.transformPoint(localbb.tr);
|
||||
const bl = m.transformPoint(localbb.bl);
|
||||
const br = m.transformPoint(localbb.br);
|
||||
|
||||
const bbc = m.transformPoint({x: 0, y: 0});
|
||||
|
||||
context.beginPath();
|
||||
context.arc(bbc.x, bbc.y, 5, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
|
||||
context.setLineDash([4, 2]);
|
||||
|
||||
// Draw main rectangle
|
||||
context.beginPath();
|
||||
context.moveTo(tl.x, tl.y);
|
||||
context.lineTo(tr.x, tr.y);
|
||||
context.lineTo(br.x, br.y);
|
||||
context.lineTo(bl.x, bl.y);
|
||||
context.lineTo(tl.x, tl.y);
|
||||
context.stroke();
|
||||
|
||||
// Draw rotation handle
|
||||
context.setLineDash([]);
|
||||
|
||||
const hm = new DOMMatrix().rotateSelf((this.rotation * 180) / Math.PI);
|
||||
const tm = m.transformPoint({x: 0, y: -this.canvas.height / 2});
|
||||
const rho = hm.transformPoint({x: 0, y: -config.rotateHandleDistance});
|
||||
const rh = {x: tm.x + rho.x, y: tm.y + rho.y};
|
||||
|
||||
let handleRadius = config.handleDrawSize / 2;
|
||||
if (this.hoveringRotateHandle(cursor.x, cursor.y, drawscale))
|
||||
handleRadius *= config.handleDrawHoverScale;
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(tm.x, tm.y);
|
||||
context.lineTo(rh.x, rh.y);
|
||||
context.stroke();
|
||||
|
||||
context.beginPath();
|
||||
context.arc(rh.x, rh.y, handleRadius, 0, 2 * Math.PI);
|
||||
context.stroke();
|
||||
|
||||
// Draw handles
|
||||
const drawHandle = (pt, hover) => {
|
||||
let hsz = config.handleDrawSize / 2;
|
||||
if (hover) hsz *= config.handleDrawHoverScale;
|
||||
|
||||
const htl = hm.transformPoint({x: -hsz, y: -hsz});
|
||||
const htr = hm.transformPoint({x: hsz, y: -hsz});
|
||||
const hbr = hm.transformPoint({x: hsz, y: hsz});
|
||||
const hbl = hm.transformPoint({x: -hsz, y: hsz});
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(htl.x + pt.x, htl.y + pt.y);
|
||||
context.lineTo(htr.x + pt.x, htr.y + pt.y);
|
||||
context.lineTo(hbr.x + pt.x, hbr.y + pt.y);
|
||||
context.lineTo(hbl.x + pt.x, hbl.y + pt.y);
|
||||
context.lineTo(htl.x + pt.x, htl.y + pt.y);
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
context.strokeStyle = "#FFF";
|
||||
context.lineWidth = 2;
|
||||
context.setLineDash([]);
|
||||
|
||||
const {ontl, ontr, onbl, onbr} = this.hoveringHandle(
|
||||
cursor.x,
|
||||
cursor.y,
|
||||
drawscale
|
||||
);
|
||||
|
||||
drawHandle(tl, ontl);
|
||||
drawHandle(tr, ontr);
|
||||
drawHandle(bl, onbl);
|
||||
drawHandle(br, onbr);
|
||||
|
||||
context.restore();
|
||||
|
||||
return () => {
|
||||
const border = config.handleDrawSize * config.handleDrawHoverScale;
|
||||
|
||||
const minx = Math.min(tl.x, tr.x, bl.x, br.x, rh.x) - border;
|
||||
const maxx = Math.max(tl.x, tr.x, bl.x, br.x, rh.x) + border;
|
||||
const miny = Math.min(tl.y, tr.y, bl.y, br.y, rh.y) - border;
|
||||
const maxy = Math.max(tl.y, tr.y, bl.y, br.y, rh.y) + border;
|
||||
|
||||
context.clearRect(minx, miny, maxx - minx, maxy - miny);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the selected image
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} context A context for rendering the image to
|
||||
* @param {CanvasRenderingContext2D} peekctx A context for rendering the layer peeking to
|
||||
* @param {object} options
|
||||
* @param {DOMMatrix} options.transform A transformation matrix to transform the position by
|
||||
* @param {number} options.opacity Opacity of the peek display
|
||||
*/
|
||||
drawImage(context, peekctx, options = {}) {
|
||||
defaultOpt(options, {
|
||||
transform: new DOMMatrix(),
|
||||
opacity: 0.4,
|
||||
});
|
||||
|
||||
context.save();
|
||||
peekctx.save();
|
||||
|
||||
const m = options.transform.multiply(this.matrix);
|
||||
|
||||
// Draw image
|
||||
context.setTransform(m);
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.canvas.width / 2,
|
||||
-this.canvas.height / 2,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
);
|
||||
|
||||
// Draw peek
|
||||
peekctx.filter = `opacity(${options.opacity * 100}%)`;
|
||||
peekctx.setTransform(m);
|
||||
peekctx.drawImage(
|
||||
this.canvas,
|
||||
-this.canvas.width / 2,
|
||||
-this.canvas.height / 2,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
);
|
||||
|
||||
peekctx.restore();
|
||||
context.restore();
|
||||
|
||||
return () => {
|
||||
// Here we only save transform for performance
|
||||
const pt = context.getTransform();
|
||||
const ppt = context.getTransform();
|
||||
|
||||
context.setTransform(m);
|
||||
peekctx.setTransform(m);
|
||||
|
||||
context.clearRect(
|
||||
-this.canvas.width / 2 - 10,
|
||||
-this.canvas.height / 2 - 10,
|
||||
this.canvas.width + 20,
|
||||
this.canvas.height + 20
|
||||
);
|
||||
|
||||
peekctx.clearRect(
|
||||
-this.canvas.width / 2 - 10,
|
||||
-this.canvas.height / 2 - 10,
|
||||
this.canvas.width + 20,
|
||||
this.canvas.height + 20
|
||||
);
|
||||
|
||||
context.setTransform(pt);
|
||||
peekctx.setTransform(ppt);
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ const selectTransformTool = () =>
|
|||
mouse.listen.world.onmousemove.on(state.movecb);
|
||||
mouse.listen.world.btn.left.onclick.on(state.clickcb);
|
||||
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
|
||||
mouse.listen.world.btn.left.ondrag.on(state.dragcb);
|
||||
mouse.listen.world.btn.left.ondragend.on(state.dragendcb);
|
||||
|
||||
// Canvas right mouse handler
|
||||
|
@ -29,12 +30,19 @@ const selectTransformTool = () =>
|
|||
keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb);
|
||||
|
||||
state.selected = null;
|
||||
|
||||
// Register Layer
|
||||
state.originalDisplayLayer = imageCollection.registerLayer(null, {
|
||||
after: uil.layer,
|
||||
category: "select-display",
|
||||
});
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear all those listeners and shortcuts we set up
|
||||
mouse.listen.world.onmousemove.clear(state.movecb);
|
||||
mouse.listen.world.btn.left.onclick.clear(state.clickcb);
|
||||
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
|
||||
mouse.listen.world.btn.left.ondrag.clear(state.dragcb);
|
||||
mouse.listen.world.btn.left.ondragend.clear(state.dragendcb);
|
||||
|
||||
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
|
||||
|
@ -55,6 +63,10 @@ const selectTransformTool = () =>
|
|||
|
||||
// Clears overlay
|
||||
imageCollection.inputElement.style.cursor = "auto";
|
||||
|
||||
// Delete Layer
|
||||
imageCollection.deleteLayer(state.originalDisplayLayer);
|
||||
state.originalDisplayLayer = null;
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
|
@ -69,7 +81,6 @@ const selectTransformTool = () =>
|
|||
state.selectionPeekOpacity = 40;
|
||||
|
||||
state.original = null;
|
||||
state.dragging = null;
|
||||
state._selected = null;
|
||||
Object.defineProperty(state, "selected", {
|
||||
get: () => state._selected,
|
||||
|
@ -80,7 +91,6 @@ const selectTransformTool = () =>
|
|||
return (state._selected = v);
|
||||
},
|
||||
});
|
||||
state.moving = null;
|
||||
|
||||
// Some things to easy request for a redraw
|
||||
state.lastMouseTarget = null;
|
||||
|
@ -97,423 +107,334 @@ const selectTransformTool = () =>
|
|||
}
|
||||
};
|
||||
|
||||
/** @type {{selected: Point, offset: Point} | null} */
|
||||
let moving = null;
|
||||
/** @type {{handle: Point} | null} */
|
||||
let scaling = null;
|
||||
let rotating = false;
|
||||
|
||||
// Clears selection and make things right
|
||||
state.reset = (erase = false) => {
|
||||
if (state.selected && !erase)
|
||||
state.originalLayer.ctx.drawImage(
|
||||
state.original.image,
|
||||
state.original.layer.ctx.drawImage(
|
||||
state.selected.canvas,
|
||||
state.original.x,
|
||||
state.original.y
|
||||
);
|
||||
|
||||
if (state.originalDisplayLayer) {
|
||||
imageCollection.deleteLayer(state.originalDisplayLayer);
|
||||
state.originalDisplayLayer = null;
|
||||
state.originalDisplayLayer.clear();
|
||||
}
|
||||
|
||||
if (state.dragging) state.dragging = null;
|
||||
else state.selected = null;
|
||||
|
||||
state.rotation = 0;
|
||||
state.original = null;
|
||||
moving = null;
|
||||
scaling = null;
|
||||
rotating = null;
|
||||
|
||||
state.redraw();
|
||||
};
|
||||
|
||||
// Selection bounding box object. Has some witchery to deal with handles.
|
||||
const selectionBB = (x1, y1, x2, y2) => {
|
||||
x1 = Math.round(x1);
|
||||
y1 = Math.round(y1);
|
||||
x2 = Math.round(x2);
|
||||
y2 = Math.round(y2);
|
||||
return {
|
||||
original: {
|
||||
x: Math.min(x1, x2),
|
||||
y: Math.min(y1, y2),
|
||||
w: Math.abs(x1 - x2),
|
||||
h: Math.abs(y1 - y2),
|
||||
},
|
||||
x: Math.min(x1, x2),
|
||||
y: Math.min(y1, y2),
|
||||
w: Math.abs(x1 - x2),
|
||||
h: Math.abs(y1 - y2),
|
||||
updateOriginal() {
|
||||
this.original.x = this.x;
|
||||
this.original.y = this.y;
|
||||
this.original.w = this.w;
|
||||
this.original.h = this.h;
|
||||
},
|
||||
contains(x, y) {
|
||||
return (
|
||||
this.x <= x &&
|
||||
x <= this.x + this.w &&
|
||||
this.y <= y &&
|
||||
y <= this.y + this.h
|
||||
);
|
||||
},
|
||||
handles() {
|
||||
const _createHandle = (x, y, originOffset = null) => {
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
scaleTo: (tx, ty, keepAspectRatio = true) => {
|
||||
const origin = {
|
||||
x: this.original.x + this.original.w / 2,
|
||||
y: this.original.y + this.original.h / 2,
|
||||
};
|
||||
let nx = tx;
|
||||
let ny = ty;
|
||||
// Selection Handlers
|
||||
const selection = _tool._draggable_selection(state);
|
||||
|
||||
let xRatio = (nx - origin.x) / (x - origin.x);
|
||||
let yRatio = (ny - origin.y) / (y - origin.y);
|
||||
if (keepAspectRatio)
|
||||
xRatio = yRatio = Math.min(xRatio, yRatio);
|
||||
// UI Erasers
|
||||
let eraseSelectedBox = () => null;
|
||||
let eraseSelectedImage = () => null;
|
||||
let eraseCursor = () => null;
|
||||
let eraseSelection = () => null;
|
||||
|
||||
if (Number.isFinite(xRatio)) {
|
||||
let left = this.original.x;
|
||||
let right = this.original.x + this.original.w;
|
||||
// Redraw UI
|
||||
state.redrawui = () => {
|
||||
// Get cursor positions
|
||||
const {x, y, sx, sy} = _tool._process_cursor(
|
||||
state.lastMouseMove,
|
||||
state.snapToGrid
|
||||
);
|
||||
|
||||
left = (left - origin.x) * xRatio + origin.x;
|
||||
right = (right - origin.x) * xRatio + origin.x;
|
||||
|
||||
this.x = left;
|
||||
this.w = right - left;
|
||||
}
|
||||
|
||||
if (Number.isFinite(yRatio)) {
|
||||
let top = this.original.y;
|
||||
let bottom = this.original.y + this.original.h;
|
||||
|
||||
top = (top - origin.y) * yRatio + origin.y;
|
||||
bottom = (bottom - origin.y) * yRatio + origin.y;
|
||||
|
||||
this.y = top;
|
||||
this.h = bottom - top;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const size = viewport.zoom * 10;
|
||||
return [
|
||||
_createHandle(this.x, this.y, size),
|
||||
_createHandle(this.x + this.w, this.y, size),
|
||||
_createHandle(this.x, this.y + this.h, size),
|
||||
_createHandle(this.x + this.w, this.y + this.h, size),
|
||||
];
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Mouse move handler. As always, also renders cursor
|
||||
state.movecb = (evn) => {
|
||||
ovLayer.clear();
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
state.erasePrevCursor && state.erasePrevCursor();
|
||||
imageCollection.inputElement.style.cursor = "auto";
|
||||
state.lastMouseTarget = evn.target;
|
||||
state.lastMouseMove = evn;
|
||||
let x = evn.x;
|
||||
let y = evn.y;
|
||||
if (state.snapToGrid) {
|
||||
x += snap(evn.x, 0, 64);
|
||||
y += snap(evn.y, 0, 64);
|
||||
}
|
||||
|
||||
const vpc = viewport.canvasToView(x, y);
|
||||
|
||||
uiCtx.save();
|
||||
|
||||
// Update scale
|
||||
if (state.scaling) {
|
||||
state.scaling.scaleTo(x, y, state.keepAspectRatio);
|
||||
}
|
||||
|
||||
// Update position
|
||||
if (state.moving) {
|
||||
state.selected.x = Math.round(x - state.moving.offset.x);
|
||||
state.selected.y = Math.round(y - state.moving.offset.y);
|
||||
state.selected.updateOriginal();
|
||||
}
|
||||
|
||||
// Draw dragging box
|
||||
if (state.dragging) {
|
||||
uiCtx.setLineDash([2, 2]);
|
||||
uiCtx.lineWidth = 1;
|
||||
uiCtx.strokeStyle = "#FFF";
|
||||
|
||||
const ix = state.dragging.ix;
|
||||
const iy = state.dragging.iy;
|
||||
|
||||
const bb = selectionBB(ix, iy, x, y);
|
||||
|
||||
const bbvp = {
|
||||
...viewport.canvasToView(bb.x, bb.y),
|
||||
w: viewport.zoom * bb.w,
|
||||
h: viewport.zoom * bb.h,
|
||||
};
|
||||
|
||||
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
|
||||
uiCtx.setLineDash([]);
|
||||
}
|
||||
eraseSelectedBox();
|
||||
|
||||
if (state.selected) {
|
||||
ovCtx.lineWidth = 1;
|
||||
ovCtx.strokeStyle = "#FFF";
|
||||
|
||||
const bb = {
|
||||
x: state.selected.x,
|
||||
y: state.selected.y,
|
||||
w: state.selected.w,
|
||||
h: state.selected.h,
|
||||
};
|
||||
|
||||
const bbvp = {
|
||||
...viewport.canvasToView(bb.x, bb.y),
|
||||
w: viewport.zoom * bb.w,
|
||||
h: viewport.zoom * bb.h,
|
||||
};
|
||||
|
||||
// Draw Image
|
||||
ovCtx.save();
|
||||
ovCtx.filter = `opacity(${state.selectionPeekOpacity}%)`;
|
||||
ovCtx.drawImage(
|
||||
state.selected.image,
|
||||
0,
|
||||
0,
|
||||
state.selected.image.width,
|
||||
state.selected.image.height,
|
||||
state.selected.x,
|
||||
state.selected.y,
|
||||
state.selected.w,
|
||||
state.selected.h
|
||||
eraseSelectedBox = state.selected.drawBox(
|
||||
uiCtx,
|
||||
{x, y},
|
||||
viewport.c2v
|
||||
);
|
||||
ovCtx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
state.originalDisplayLayer.clear();
|
||||
state.originalDisplayLayer.ctx.save();
|
||||
state.originalDisplayLayer.ctx.drawImage(
|
||||
state.selected.image,
|
||||
0,
|
||||
0,
|
||||
state.selected.image.width,
|
||||
state.selected.image.height,
|
||||
state.selected.x,
|
||||
state.selected.y,
|
||||
state.selected.w,
|
||||
state.selected.h
|
||||
// Mouse Move Handler
|
||||
state.movecb = (evn) => {
|
||||
state.lastMouseMove = evn;
|
||||
|
||||
// Get cursor positions
|
||||
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||
|
||||
// Erase Cursor
|
||||
eraseSelectedBox();
|
||||
eraseSelectedImage();
|
||||
eraseSelection();
|
||||
eraseCursor();
|
||||
imageCollection.inputElement.style.cursor = "default";
|
||||
|
||||
// Draw Box and Selected Image
|
||||
if (state.selected) {
|
||||
eraseSelectedBox = state.selected.drawBox(
|
||||
uiCtx,
|
||||
{x, y},
|
||||
viewport.c2v
|
||||
);
|
||||
state.originalDisplayLayer.ctx.restore();
|
||||
|
||||
// Draw selection box
|
||||
uiCtx.strokeStyle = "#FFF";
|
||||
uiCtx.setLineDash([4, 2]);
|
||||
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
|
||||
uiCtx.setLineDash([]);
|
||||
|
||||
// Draw Scaling/Rotation Origin
|
||||
uiCtx.beginPath();
|
||||
uiCtx.arc(
|
||||
bbvp.x + bbvp.w / 2,
|
||||
bbvp.y + bbvp.h / 2,
|
||||
5,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
uiCtx.stroke();
|
||||
|
||||
// Draw Scaling Handles
|
||||
let cursorInHandle = false;
|
||||
state.selected.handles().forEach((handle) => {
|
||||
const bbvph = {
|
||||
...viewport.canvasToView(handle.x, handle.y),
|
||||
w: 10,
|
||||
h: 10,
|
||||
};
|
||||
|
||||
bbvph.x -= 5;
|
||||
bbvph.y -= 5;
|
||||
|
||||
const inhandle =
|
||||
evn.evn.clientX > bbvph.x &&
|
||||
evn.evn.clientX < bbvph.x + bbvph.w &&
|
||||
evn.evn.clientY > bbvph.y &&
|
||||
evn.evn.clientY < bbvph.y + bbvph.h;
|
||||
|
||||
if (inhandle) {
|
||||
cursorInHandle = true;
|
||||
uiCtx.strokeRect(
|
||||
bbvph.x - 1,
|
||||
bbvph.y - 1,
|
||||
bbvph.w + 2,
|
||||
bbvph.h + 2
|
||||
);
|
||||
} else {
|
||||
uiCtx.strokeRect(bbvph.x, bbvph.y, bbvph.w, bbvph.h);
|
||||
}
|
||||
});
|
||||
|
||||
// Change cursor
|
||||
if (cursorInHandle || state.selected.contains(evn.x, evn.y))
|
||||
if (
|
||||
state.selected.hoveringBox(x, y) ||
|
||||
state.selected.hoveringHandle(x, y, viewport.zoom).onHandle ||
|
||||
state.selected.hoveringRotateHandle(x, y, viewport.zoom)
|
||||
) {
|
||||
imageCollection.inputElement.style.cursor = "pointer";
|
||||
}
|
||||
|
||||
eraseSelectedImage = state.selected.drawImage(
|
||||
state.originalDisplayLayer.ctx,
|
||||
ovCtx,
|
||||
{opacity: state.selectionPeekOpacity / 100}
|
||||
);
|
||||
}
|
||||
|
||||
// Draw current cursor location
|
||||
state.erasePrevCursor = _tool._cursor_draw(x, y);
|
||||
// Draw Selection
|
||||
if (selection.exists) {
|
||||
uiCtx.save();
|
||||
uiCtx.setLineDash([2, 2]);
|
||||
uiCtx.lineWidth = 2;
|
||||
uiCtx.strokeStyle = "#FFF";
|
||||
|
||||
uiCtx.restore();
|
||||
const bbvp = selection.bb.transform(viewport.c2v);
|
||||
uiCtx.beginPath();
|
||||
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
|
||||
uiCtx.stroke();
|
||||
|
||||
eraseSelection = () =>
|
||||
uiCtx.clearRect(
|
||||
bbvp.x - 10,
|
||||
bbvp.y - 10,
|
||||
bbvp.w + 20,
|
||||
bbvp.h + 20
|
||||
);
|
||||
|
||||
uiCtx.restore();
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
eraseCursor = _tool._cursor_draw(sx, sy);
|
||||
};
|
||||
|
||||
// Handles left mouse clicks
|
||||
state.clickcb = (evn) => {
|
||||
if (
|
||||
!state.original ||
|
||||
(state.originalLayer === uil.layer &&
|
||||
state.original.x === state.selected.x &&
|
||||
state.original.y === state.selected.y &&
|
||||
state.original.w === state.selected.w &&
|
||||
state.original.h === state.selected.h)
|
||||
state.selected &&
|
||||
!(
|
||||
state.selected.rotation === 0 &&
|
||||
state.selected.scale.x === 1 &&
|
||||
state.selected.scale.y === 1 &&
|
||||
state.selected.position.x === state.original.sx &&
|
||||
state.selected.position.y === state.original.sy &&
|
||||
state.original.layer === uil.layer
|
||||
)
|
||||
) {
|
||||
state.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// If something is selected, commit changes to the canvas
|
||||
if (state.selected) {
|
||||
state.originalLayer.ctx.drawImage(
|
||||
state.selected.image,
|
||||
// Put original image back
|
||||
state.original.layer.ctx.drawImage(
|
||||
state.selected.canvas,
|
||||
state.original.x,
|
||||
state.original.y
|
||||
);
|
||||
commands.runCommand("eraseImage", "Image Transform Erase", {
|
||||
...state.original,
|
||||
ctx: state.originalLayer.ctx,
|
||||
|
||||
// Erase Original Selection Area
|
||||
commands.runCommand("eraseImage", "Transform Tool Erase", {
|
||||
ctx: state.original.layer.ctx,
|
||||
x: state.original.x,
|
||||
y: state.original.y,
|
||||
w: state.selected.canvas.width,
|
||||
h: state.selected.canvas.height,
|
||||
});
|
||||
commands.runCommand("drawImage", "Image Transform Draw", {
|
||||
image: state.selected.image,
|
||||
x: Math.round(state.selected.x),
|
||||
y: Math.round(state.selected.y),
|
||||
w: Math.round(state.selected.w),
|
||||
h: Math.round(state.selected.h),
|
||||
|
||||
// Draw Image
|
||||
const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, {
|
||||
border: 10,
|
||||
});
|
||||
commands.runCommand("drawImage", "Transform Tool Apply", {
|
||||
image: canvas,
|
||||
...bb,
|
||||
});
|
||||
|
||||
state.reset(true);
|
||||
} else {
|
||||
state.reset();
|
||||
}
|
||||
};
|
||||
|
||||
// Handles left mouse drag start events
|
||||
state.dragstartcb = (evn) => {
|
||||
const {
|
||||
x: ix,
|
||||
y: iy,
|
||||
sx: six,
|
||||
sy: siy,
|
||||
} = _tool._process_cursor({x: evn.ix, y: evn.iy}, state.snapToGrid);
|
||||
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||
|
||||
if (state.selected) {
|
||||
const hoveringBox = state.selected.hoveringBox(ix, iy);
|
||||
const hoveringHandle = state.selected.hoveringHandle(
|
||||
ix,
|
||||
iy,
|
||||
viewport.zoom
|
||||
);
|
||||
const hoveringRotateHandle = state.selected.hoveringRotateHandle(
|
||||
ix,
|
||||
iy,
|
||||
viewport.zoom
|
||||
);
|
||||
|
||||
if (hoveringBox) {
|
||||
// Start dragging
|
||||
moving = {
|
||||
selected: state.selected.position,
|
||||
offset: {
|
||||
x: six - state.selected.position.x,
|
||||
y: siy - state.selected.position.y,
|
||||
},
|
||||
};
|
||||
return;
|
||||
} else if (hoveringHandle.onHandle) {
|
||||
// Start scaling
|
||||
let handle = {x: 0, y: 0};
|
||||
|
||||
const lbb = new BoundingBox({
|
||||
x: -state.selected.canvas.width / 2,
|
||||
y: -state.selected.canvas.height / 2,
|
||||
w: state.selected.canvas.width,
|
||||
h: state.selected.canvas.height,
|
||||
});
|
||||
|
||||
if (hoveringHandle.ontl) {
|
||||
handle = lbb.tl;
|
||||
} else if (hoveringHandle.ontr) {
|
||||
handle = lbb.tr;
|
||||
} else if (hoveringHandle.onbl) {
|
||||
handle = lbb.bl;
|
||||
} else {
|
||||
handle = lbb.br;
|
||||
}
|
||||
|
||||
scaling = {
|
||||
handle,
|
||||
};
|
||||
return;
|
||||
} else if (hoveringRotateHandle) {
|
||||
rotating = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
selection.dragstartcb(evn);
|
||||
};
|
||||
|
||||
const transform = (evn, x, y, sx, sy) => {
|
||||
if (moving) {
|
||||
state.selected.position = {
|
||||
x: sx - moving.offset.x,
|
||||
y: sy - moving.offset.y,
|
||||
};
|
||||
}
|
||||
|
||||
if (scaling) {
|
||||
/** @type {DOMMatrix} */
|
||||
const m = state.selected.rtmatrix.invertSelf();
|
||||
const lscursor = m.transformPoint({x: sx, y: sy});
|
||||
|
||||
const xs = lscursor.x / scaling.handle.x;
|
||||
const xy = lscursor.y / scaling.handle.y;
|
||||
|
||||
if (!state.keepAspectRatio) state.selected.scale = {x: xs, y: xy};
|
||||
else {
|
||||
const scale = Math.max(xs, xy);
|
||||
state.selected.scale = {x: scale, y: scale};
|
||||
}
|
||||
}
|
||||
|
||||
if (rotating) {
|
||||
const center = state.selected.matrix.transformPoint({x: 0, y: 0});
|
||||
let angle = Math.atan2(x - center.x, center.y - y);
|
||||
|
||||
if (evn.evn.shiftKey)
|
||||
angle =
|
||||
config.rotationSnappingAngles.find(
|
||||
(v) => Math.abs(v - angle) < config.rotationSnappingDistance
|
||||
) ?? angle;
|
||||
|
||||
state.selected.rotation = angle;
|
||||
}
|
||||
};
|
||||
|
||||
// Handles left mouse drag events
|
||||
state.dragstartcb = (evn) => {
|
||||
let ix = evn.ix;
|
||||
let iy = evn.iy;
|
||||
if (state.snapToGrid) {
|
||||
ix += snap(evn.ix, 0, 64);
|
||||
iy += snap(evn.iy, 0, 64);
|
||||
}
|
||||
state.dragcb = (evn) => {
|
||||
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||
|
||||
// If is selected, check if drag is in handles/body and act accordingly
|
||||
if (state.selected) {
|
||||
const handles = state.selected.handles();
|
||||
if (state.selected) transform(evn, x, y, sx, sy);
|
||||
|
||||
const activeHandle = handles.find((v) => {
|
||||
const vpc = viewport.canvasToView(v.x, v.y);
|
||||
const tlc = viewport.viewToCanvas(vpc.x - 5, vpc.y - 5);
|
||||
const brc = viewport.viewToCanvas(vpc.x + 5, vpc.y + 5);
|
||||
const bb = {
|
||||
x: tlc.x,
|
||||
y: tlc.y,
|
||||
w: brc.x - tlc.x,
|
||||
h: brc.y - tlc.y,
|
||||
};
|
||||
|
||||
return (
|
||||
evn.ix > bb.x &&
|
||||
evn.ix < bb.x + bb.w &&
|
||||
evn.iy > bb.y &&
|
||||
evn.iy < bb.y + bb.h
|
||||
);
|
||||
});
|
||||
if (activeHandle) {
|
||||
state.scaling = activeHandle;
|
||||
return;
|
||||
} else if (state.selected.contains(ix, iy)) {
|
||||
state.moving = {
|
||||
offset: {x: ix - state.selected.x, y: iy - state.selected.y},
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If it is not, just create new selection
|
||||
state.reset();
|
||||
state.dragging = {ix, iy};
|
||||
if (selection.exists) selection.dragcb(evn);
|
||||
};
|
||||
|
||||
// Handles left mouse drag end events
|
||||
state.dragendcb = (evn) => {
|
||||
let x = evn.x;
|
||||
let y = evn.y;
|
||||
if (state.snapToGrid) {
|
||||
x += snap(evn.x, 0, 64);
|
||||
y += snap(evn.y, 0, 64);
|
||||
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||
|
||||
if (selection.exists) {
|
||||
selection.dragendcb(evn);
|
||||
|
||||
const bb = selection.bb;
|
||||
|
||||
state.reset();
|
||||
|
||||
if (selection.exists && bb.w !== 0 && bb.h !== 0) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = bb.w;
|
||||
canvas.height = bb.h;
|
||||
canvas
|
||||
.getContext("2d")
|
||||
.drawImage(
|
||||
uil.canvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
0,
|
||||
bb.w,
|
||||
bb.h
|
||||
);
|
||||
|
||||
uil.ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
|
||||
|
||||
state.original = {
|
||||
...bb,
|
||||
sx: selection.bb.center.x,
|
||||
sy: selection.bb.center.y,
|
||||
layer: uil.layer,
|
||||
};
|
||||
state.selected = new _tool.MarqueeSelection(canvas, bb.center);
|
||||
}
|
||||
|
||||
selection.deselect();
|
||||
}
|
||||
|
||||
// If we are scaling, stop scaling and do some handler magic
|
||||
if (state.scaling) {
|
||||
state.selected.updateOriginal();
|
||||
state.scaling = null;
|
||||
// If we are moving the selection, just... stop
|
||||
} else if (state.moving) {
|
||||
state.moving = null;
|
||||
/**
|
||||
* If we are dragging, create a cutout selection area and save to an auxiliar image
|
||||
* We will be rendering the image to the overlay, so it will not be noticeable
|
||||
*/
|
||||
} else if (state.dragging) {
|
||||
state.original = selectionBB(
|
||||
state.dragging.ix,
|
||||
state.dragging.iy,
|
||||
x,
|
||||
y
|
||||
);
|
||||
state.selected = selectionBB(
|
||||
state.dragging.ix,
|
||||
state.dragging.iy,
|
||||
x,
|
||||
y
|
||||
);
|
||||
state.originalLayer = uil.layer;
|
||||
state.originalDisplayLayer = imageCollection.registerLayer(null, {
|
||||
after: uil.layer,
|
||||
category: "select-display",
|
||||
});
|
||||
if (state.selected) transform(evn, x, y, sx, sy);
|
||||
|
||||
// Cut out selected portion of the image for manipulation
|
||||
const cvs = document.createElement("canvas");
|
||||
cvs.width = state.selected.w;
|
||||
cvs.height = state.selected.h;
|
||||
const ctx = cvs.getContext("2d");
|
||||
moving = null;
|
||||
scaling = null;
|
||||
rotating = false;
|
||||
|
||||
ctx.drawImage(
|
||||
uil.canvas,
|
||||
state.selected.x,
|
||||
state.selected.y,
|
||||
state.selected.w,
|
||||
state.selected.h,
|
||||
0,
|
||||
0,
|
||||
state.selected.w,
|
||||
state.selected.h
|
||||
);
|
||||
|
||||
uil.ctx.clearRect(
|
||||
state.selected.x,
|
||||
state.selected.y,
|
||||
state.selected.w,
|
||||
state.selected.h
|
||||
);
|
||||
state.selected.image = cvs;
|
||||
state.original.image = cvs;
|
||||
|
||||
if (state.selected.w === 0 || state.selected.h === 0)
|
||||
state.selected = null;
|
||||
|
||||
state.dragging = null;
|
||||
}
|
||||
state.redraw();
|
||||
};
|
||||
|
||||
|
@ -550,11 +471,11 @@ const selectTransformTool = () =>
|
|||
|
||||
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
|
||||
ctx.drawImage(
|
||||
state.selected.image,
|
||||
state.selected.canvas,
|
||||
0,
|
||||
0,
|
||||
state.selected.image.width,
|
||||
state.selected.image.height,
|
||||
state.selected.canvas.width,
|
||||
state.selected.canvas.height,
|
||||
0,
|
||||
0,
|
||||
state.selected.w,
|
||||
|
@ -684,7 +605,7 @@ const selectTransformTool = () =>
|
|||
saveSelectionButton.onclick = () => {
|
||||
downloadCanvas({
|
||||
cropToContent: false,
|
||||
canvas: state.selected.image,
|
||||
canvas: state.selected.canvas,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -695,7 +616,7 @@ const selectTransformTool = () =>
|
|||
createResourceButton.title = "Saves Selection as a Resource";
|
||||
createResourceButton.onclick = () => {
|
||||
const image = document.createElement("img");
|
||||
image.src = state.selected.image.toDataURL();
|
||||
image.src = state.selected.canvas.toDataURL();
|
||||
image.onload = () => {
|
||||
tools.stamp.state.addResource("Selection Resource", image);
|
||||
tools.stamp.enable();
|
||||
|
|
|
@ -1,3 +1,41 @@
|
|||
/**
|
||||
* Generic wheel handler
|
||||
*/
|
||||
let _stamp_wheel_accum = 0;
|
||||
|
||||
const _stamp_onwheel = (evn, state) => {
|
||||
if (evn.mode !== WheelEvent.DOM_DELTA_PIXEL) {
|
||||
// We don't really handle non-pixel scrolling
|
||||
return;
|
||||
}
|
||||
|
||||
let delta = evn.delta;
|
||||
if (evn.evn.shiftKey) delta *= 0.01;
|
||||
|
||||
// A simple but (I hope) effective fix for mouse wheel behavior
|
||||
_stamp_wheel_accum += delta;
|
||||
|
||||
if (
|
||||
!evn.evn.shiftKey &&
|
||||
Math.abs(_stamp_wheel_accum) > config.wheelTickSize
|
||||
) {
|
||||
// Snap to next or previous position
|
||||
const v =
|
||||
state.scale - 0.1 * (_stamp_wheel_accum / Math.abs(_stamp_wheel_accum));
|
||||
|
||||
state.setScale(v + snap(v, 0, 0.1));
|
||||
state.redraw(evn);
|
||||
|
||||
_stamp_wheel_accum = 0; // Zero accumulation
|
||||
} else if (evn.evn.shiftKey && Math.abs(_stamp_wheel_accum) >= 1) {
|
||||
const v = state.scale - _stamp_wheel_accum * 0.01;
|
||||
state.setScale(v);
|
||||
state.redraw(evn);
|
||||
|
||||
_stamp_wheel_accum = 0; // Zero accumulation
|
||||
}
|
||||
};
|
||||
|
||||
const stampTool = () =>
|
||||
toolbar.registerTool(
|
||||
"./res/icons/file-up.svg",
|
||||
|
@ -14,6 +52,12 @@ const stampTool = () =>
|
|||
mouse.listen.world.btn.left.onclick.on(state.drawcb);
|
||||
mouse.listen.world.btn.right.onclick.on(state.cancelcb);
|
||||
|
||||
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
|
||||
mouse.listen.world.btn.left.ondrag.on(state.dragcb);
|
||||
mouse.listen.world.btn.left.ondragend.on(state.dragendcb);
|
||||
|
||||
mouse.listen.world.onwheel.on(state.onwheelcb);
|
||||
|
||||
// For calls from other tools to paste image
|
||||
if (opt && opt.image) {
|
||||
state.addResource(
|
||||
|
@ -41,6 +85,12 @@ const stampTool = () =>
|
|||
mouse.listen.world.btn.left.onclick.clear(state.drawcb);
|
||||
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
|
||||
|
||||
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
|
||||
mouse.listen.world.btn.left.ondrag.clear(state.dragcb);
|
||||
mouse.listen.world.btn.left.ondragend.clear(state.dragendcb);
|
||||
|
||||
mouse.listen.world.onwheel.clear(state.onwheelcb);
|
||||
|
||||
ovLayer.clear();
|
||||
},
|
||||
{
|
||||
|
@ -54,7 +104,15 @@ const stampTool = () =>
|
|||
state.lastMouseMove = {x: 0, y: 0};
|
||||
state.block_res_change = true;
|
||||
|
||||
// Current Rotation
|
||||
let rotation = 0;
|
||||
let rotating = null;
|
||||
// Current Scale
|
||||
state.scale = 1;
|
||||
|
||||
state.selectResource = (resource, nolock = true, deselect = true) => {
|
||||
rotation = 0;
|
||||
state.setScale(1);
|
||||
if (nolock && state.ctxmenu.uploadButton.disabled) return;
|
||||
|
||||
console.debug(
|
||||
|
@ -290,32 +348,65 @@ const stampTool = () =>
|
|||
syncResources();
|
||||
};
|
||||
|
||||
state.movecb = (evn) => {
|
||||
let x = evn.x;
|
||||
let y = evn.y;
|
||||
if (state.snapToGrid) {
|
||||
x += snap(evn.x, 0, 64);
|
||||
y += snap(evn.y, 0, 64);
|
||||
state.onwheelcb = (evn) => {
|
||||
_stamp_onwheel(evn, state);
|
||||
};
|
||||
|
||||
state.dragstartcb = (evn) => {
|
||||
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||
rotating = {x: sx, y: sy};
|
||||
};
|
||||
|
||||
state.dragcb = (evn) => {
|
||||
if (rotating) {
|
||||
rotation = Math.atan2(rotating.x - evn.x, evn.y - rotating.y);
|
||||
|
||||
if (evn.evn.shiftKey)
|
||||
rotation =
|
||||
config.rotationSnappingAngles.find(
|
||||
(v) =>
|
||||
Math.abs(v - rotation) < config.rotationSnappingDistance
|
||||
) ?? rotation;
|
||||
}
|
||||
};
|
||||
|
||||
const vpc = viewport.canvasToView(x, y);
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
state.erasePrevCursor && state.erasePrevCursor();
|
||||
state.dragendcb = (evn) => {
|
||||
rotating = null;
|
||||
};
|
||||
|
||||
uiCtx.save();
|
||||
let erasePrevCursor = () => null;
|
||||
|
||||
state.movecb = (evn) => {
|
||||
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||
|
||||
// Erase Previous Cursors
|
||||
erasePrevCursor();
|
||||
|
||||
state.lastMouseMove = evn;
|
||||
|
||||
ovLayer.clear();
|
||||
|
||||
let px = sx;
|
||||
let py = sy;
|
||||
|
||||
if (rotating) {
|
||||
px = rotating.x;
|
||||
py = rotating.y;
|
||||
}
|
||||
|
||||
// Draw selected image
|
||||
if (state.selected) {
|
||||
ovCtx.drawImage(state.selected.image, x, y);
|
||||
ovCtx.save();
|
||||
ovCtx.translate(px, py);
|
||||
ovCtx.scale(state.scale, state.scale);
|
||||
ovCtx.rotate(rotation);
|
||||
|
||||
ovCtx.drawImage(state.selected.image, 0, 0);
|
||||
ovCtx.restore();
|
||||
}
|
||||
|
||||
// Draw current cursor location
|
||||
state.erasePrevCursor = _tool._cursor_draw(x, y);
|
||||
uiCtx.restore();
|
||||
erasePrevCursor = _tool._cursor_draw(px, py);
|
||||
};
|
||||
|
||||
state.redraw = () => {
|
||||
|
@ -323,20 +414,16 @@ const stampTool = () =>
|
|||
};
|
||||
|
||||
state.drawcb = (evn) => {
|
||||
let x = evn.x;
|
||||
let y = evn.y;
|
||||
if (state.snapToGrid) {
|
||||
x += snap(evn.x, 0, 64);
|
||||
y += snap(evn.y, 0, 64);
|
||||
}
|
||||
const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid);
|
||||
|
||||
const resource = state.selected;
|
||||
|
||||
if (resource) {
|
||||
const {canvas, bb} = cropCanvas(ovCanvas, {border: 10});
|
||||
commands.runCommand("drawImage", "Image Stamp", {
|
||||
image: resource.image,
|
||||
x,
|
||||
y,
|
||||
image: canvas,
|
||||
x: bb.x,
|
||||
y: bb.y,
|
||||
});
|
||||
|
||||
if (resource.temporary) {
|
||||
|
@ -380,6 +467,16 @@ const stampTool = () =>
|
|||
);
|
||||
state.ctxmenu.snapToGridLabel = array;
|
||||
|
||||
// Scale Slider
|
||||
const scaleSlider = _toolbar_input.slider(state, "scale", "Scale", {
|
||||
min: 0.01,
|
||||
max: 10,
|
||||
step: 0.1,
|
||||
textStep: 0.001,
|
||||
});
|
||||
state.ctxmenu.scaleSlider = scaleSlider.slider;
|
||||
state.setScale = scaleSlider.setValue;
|
||||
|
||||
// Create resource list
|
||||
const uploadButtonId = `upload-btn-${guid()}`;
|
||||
|
||||
|
@ -528,6 +625,7 @@ const stampTool = () =>
|
|||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
menu.appendChild(state.ctxmenu.scaleSlider);
|
||||
menu.appendChild(state.ctxmenu.resourceManager);
|
||||
},
|
||||
shortcut: "U",
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
<title>openOutpaint 🐠</title>
|
||||
<!-- CSS Variables -->
|
||||
<link href="../css/colors.css?v=3f81e80" rel="stylesheet" />
|
||||
<link href="../css/icons.css?v=caa702e" rel="stylesheet" />
|
||||
<link href="../css/icons.css?v=9ae0466" rel="stylesheet" />
|
||||
|
||||
<link href="../css/index.css?v=5b8d4d6" rel="stylesheet" />
|
||||
<link href="../css/layers.css?v=b4fbf61" rel="stylesheet" />
|
||||
<link href="../css/layers.css?v=92c0352" rel="stylesheet" />
|
||||
|
||||
<link href="../css/ui/generic.css?v=4b9afe2" rel="stylesheet" />
|
||||
<link href="../css/ui/generic.css?v=802bd41" rel="stylesheet" />
|
||||
|
||||
<link href="../css/ui/history.css?v=0b03861" rel="stylesheet" />
|
||||
<link href="../css/ui/layers.css?v=4fd95fe" rel="stylesheet" />
|
||||
<link href="../css/ui/layers.css?v=ae472cd" rel="stylesheet" />
|
||||
<link href="../css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
|
||||
|
||||
<!-- Tool Specific CSS -->
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
<iframe
|
||||
id="openoutpaint"
|
||||
style="width: 100%; height: 800px"
|
||||
src="../index.html?v=95a96ad"
|
||||
src="../index.html?v=95a96ad"
|
||||
src="../index.html?v=daf18de"
|
||||
src="../index.html?v=daf18de"
|
||||
frameborder="0"></iframe>
|
||||
<button id="add-res">Add Resource</button>
|
||||
<script>
|
||||
|
|
Loading…
Reference in a new issue