Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!doctype html>
- <html>
- <head>
- <style type="text/css">
- #container {
- width:800px;
- height:600px;
- border:1px solid lightgray;
- }
- #canvas {
- width:100%;
- height:100%;
- }
- </style>
- <script type="text/javascript">
- class Box {
- constructor(args) {
- this.left = args.left;
- this.top = args.top;
- this.width = args.width;
- this.height = args.height;
- }
- containsPoint(x, y) {
- let contains =
- x >= this.left
- && x <= this.left + this.width
- && y >= this.top
- && y <= this.top + this.height
- ;
- return contains;
- }
- };
- class Rect {
- constructor(x, y, w, h, z) {
- this.x = x;
- this.y = y;
- this.width = w;
- this.height = h;
- this.z = z === undefined ? 0 : z;
- }
- moveTo(x, y) {
- this.x = x;
- this.y = y;
- }
- moveBy(dx, dy) {
- this.moveTo(this.x + dx, this.y + dy);
- }
- resize(w, h) {
- this.width = w;
- this.height = h;
- }
- getX() {
- return this.x;
- }
- getY() {
- return this.y;
- }
- getRight() {
- return this.x + this.width;
- }
- getBottom() {
- return this.y + this.height;
- }
- getWidth() {
- return this.width;
- }
- getHeight() {
- return this.height;
- }
- _draw(ctx, bbox) {
- ctx.beginPath();
- ctx.rect(bbox.left + 0.5, bbox.top + 0.5, bbox.width, bbox.height);
- }
- drawGhost(ctx, bbox) {
- this._draw(ctx, bbox);
- ctx.stroke();
- }
- draw(ctx) {
- ctx.fillStyle='white';
- this._draw(ctx, this.bbox());
- ctx.fill();
- ctx.stroke();
- }
- zIndex() {
- return this.z;
- }
- containsPoint(x, y) {
- let contains =
- x >= this.x
- && x <= this.x + this.width
- && y >= this.y
- && y <= this.y + this.height;
- return contains;
- }
- bbox() {
- return {
- left:this.x,
- top:this.y,
- width:this.width,
- height:this.height
- };
- }
- };
- class Oval extends Rect {
- constructor(x, y, w, h, z) {
- super(x, y, w, h, z);
- }
- _drawEllipse(ctx, bbox) {
- let rx = bbox.width /2;
- let ry = bbox.height /2;
- if(rx <= 0 || ry <=0) {
- return;
- }
- let cx = bbox.left + rx;
- let cy = bbox.top + ry;
- ctx.beginPath();
- ctx.ellipse(cx + 0.5, cy + 0.5, rx, ry, 0, 0, 2 * Math.PI);
- }
- draw(ctx) {
- this._drawEllipse(ctx, this.bbox());
- ctx.fillStyle='pink';
- ctx.fill();
- ctx.stroke();
- }
- drawGhost(ctx, bbox) {
- this._drawEllipse(ctx, bbox);
- ctx.stroke();
- }
- };
- class Circle extends Oval {
- constructor(x, y, r, z) {
- super(x, y, r*2, r*2, z);
- }
- resize(w, h) {
- let s = Math.max(w,h);
- super.resize(s,s);
- }
- };
- class Group extends Rect {
- constructor(items) {
- let left = items.reduce((r, i) => r.getX() < i.getX() ? r : i).getX();
- let top = items.reduce((r, i) => r.getY() < i.getY() ? r : i).getY();
- let right = items.reduce((r, i) => r.getRight() > i.getRight() ? r : i).getRight();
- let bottom = items.reduce((r, i) => r.getBottom() > i.getBottom() ? r : i).getBottom();
- super(left, top, right-left, bottom-top);
- this.items = items;
- }
- moveTo(x, y) {
- for(let item of this.items){
- let dx = item.getX() - this.x;
- let dy = item.getY() - this.y;
- item.moveTo(x + dx, y + dy);
- }
- this.x = x;
- this.y = y;
- }
- resize(w,h) {
- if(w < 0) {
- throw Error('negative w');
- }
- if(h < 0) {
- throw Error('negative h');
- }
- let rx = w / this.width;
- let ry = h / this.height;
- this.width = - Infinity;
- this.height = - Infinity;
- for(let item of this.items) {
- let dx = item.getX() - this.x;
- let dy = item.getY() - this.y;
- item.resize(Math.round(item.getWidth() * rx), Math.round(item.getHeight() * ry));
- this.width = Math.max(dx + item.getWidth(), this.width);
- this.height = Math.max(dx + item.getHeight(), this.height);
- item.moveTo(
- Math.round(this.x + dx * rx),
- Math.round(this.y + dy * ry)
- );
- }
- }
- draw(ctx) {
- for(let item of this.items) {
- item.draw(ctx);
- }
- }
- drawGhost(ctx, bbox) {
- let rx = bbox.width / this.width;
- let ry = bbox.height / this.height;
- for(let item of this.items) {
- let dx = item.getX() - this.x;
- let dy = item.getY() - this.y;
- let itembbox = {
- left : bbox.left + dx * rx,
- top : bbox.top + dy *ry,
- width : item.getWidth() * rx,
- height : item.getHeight() * ry
- };
- item.drawGhost(ctx, itembbox);
- }
- }
- };
- function samePos(a, b) {
- return a.x == b.x && a.y == b.y;
- }
- class DragAction {
- constructor(originX, originY) {
- if (new.target === DragAction) {
- throw new TypeError("Cannot instanciate an abstract class");
- }
- this.origin = {x:originX, y:originY};
- this.dragOffset = {x:0,y:0};
- this.hasMoved_ = false;
- }
- update(x, y) {
- this.dragOffset = {
- x : x - this.origin.x,
- y : y - this.origin.y
- };
- this.hasMoved_ = true;
- }
- hasMoved() {
- return this.hasMoved_;
- }
- };
- class SelectAction extends DragAction {
- constructor(originX, originY, u) {
- super(originX, originY);
- this.u = u;
- }
- drawGhost(ctx) {
- ctx.setLineDash([3]);
- ctx.beginPath();
- ctx.rect(this.origin.x + 0.5, this.origin.y + 0.5, this.dragOffset.x, this.dragOffset.y);
- ctx.stroke();
- ctx.setLineDash([]);
- }
- done() {
- let outer = this.dragOffset.x < 0 || this.dragOffset.y < 0;
- let bbox = {
- left: Math.min(this.origin.x, this.origin.x + this.dragOffset.x),
- top: Math.min(this.origin.y, this.origin.y + this.dragOffset.y),
- width:Math.abs(this.dragOffset.x),
- height:Math.abs(this.dragOffset.y)
- };
- if(outer) {
- this.u.selectedItems = this.u.findItemsOverlapping(bbox);
- } else {
- this.u.selectedItems = this.u.findItemsInside(bbox);
- }
- }
- };
- class MoveAction extends DragAction {
- constructor(originX, originY, item) {
- super(originX, originY);
- this.item = item;
- document.body.style.cursor = 'move';
- }
- drawGhost(ctx) {
- let bbox = {
- left : this.item.getX() + this.dragOffset.x,
- top : this.item.getY() + this.dragOffset.y,
- width : this.item.getWidth(),
- height : this.item.getHeight()
- };
- this.item.drawGhost(ctx, bbox);
- }
- done() {
- this.item.moveBy(this.dragOffset.x, this.dragOffset.y);
- document.body.style.cursor = 'auto';
- }
- };
- class ResizeAction extends DragAction {
- constructor(originX, originY, item, direction) {
- super(originX, originY);
- this.item = item;
- this.bboxFn = {
- 'topLeft' : (dx, dy) => { return {
- left: item.getX() + dx,
- top: item.getY() + dy,
- width: item.getWidth() - dx,
- height:item.getHeight() - dy
- }},
- 'topRight' : (dx, dy) => { return {
- left: item.getX(),
- top: item.getY() + dy,
- width: item.getWidth() + dx,
- height:item.getHeight() - dy
- }},
- 'bottomLeft' : (dx, dy) => { return {
- left: item.getX() + dx,
- top: item.getY(),
- width: item.getWidth() - dx,
- height: item.getHeight() + dy
- }},
- 'bottomRight' : (dx, dy) => { return {
- left: item.getX(),
- top: item.getY(),
- width: item.getWidth() + dx,
- height: item.getHeight() + dy
- }}
- }[direction];
- document.body.style.cursor = {
- 'topLeft': 'se-resize',
- 'topRight': 'sw-resize',
- 'bottomLeft': 'ne-resize',
- 'bottomRight': 'nw-resize',
- }[direction];
- }
- drawGhost(ctx) {
- let bbox = this.bboxFn(this.dragOffset.x, this.dragOffset.y);
- this.item.drawGhost(ctx, bbox);
- }
- done() {
- let bbox = this.bboxFn(this.dragOffset.x, this.dragOffset.y);
- this.item.resize(bbox.width, bbox.height);
- this.item.moveTo(bbox.left, bbox.top);
- document.body.style.cursor = 'auto';
- }
- };
- const HANDLE_SIZE = 8;
- class ItemsView {
- constructor(ctx) {
- this.ctx = ctx;
- this.drawables = [];
- const that = this;
- //install mouse handlers
- ctx.canvas.onmousemove = (e) => {
- that.dispatch('mousemove', that.fixEvent(e));
- };
- ctx.canvas.onmousedown = (e) => {
- that.dispatch('mousedown', that.fixEvent(e));
- };
- ctx.canvas.onmouseup = (e) => {
- that.dispatch('mouseup', that.fixEvent(e));
- };
- // selected items
- this.selectedItems = [];
- this.dragState = 'idle';
- this.dragAction = null;
- }
- findItemsOverlapping(bbox) {
- let box = new Box(bbox);
- return this.drawables.filter((item) => {
- return box.containsPoint(item.getX(), item.getY())
- || box.containsPoint(item.getX() + item.getWidth(), item.getY())
- || box.containsPoint(item.getX(), item.getY() + item.getHeight())
- || box.containsPoint(item.getX() + item.getWidth(), item.getY() + item.getHeight());
- });
- }
- findItemsInside(bbox) {
- let box = new Box(bbox);
- return this.drawables.filter((item) => {
- return box.containsPoint(item.getX(), item.getY())
- && box.containsPoint(item.getX() + item.getWidth(), item.getY() + item.getHeight());
- });
- }
- /* recompute correct mouse coordinates */
- fixEvent(e) {
- let canvas = this.ctx.canvas;
- let bounds = canvas.getBoundingClientRect();
- let x = canvas.width * ((e.pageX - bounds.left - window.scrollX) / bounds.width);
- let y = canvas.height * ((e.pageY - bounds.top - window.scrollY) / bounds.height);
- return {
- x:Math.round(x),
- y:Math.round(y),
- button:e.button,
- ctrlKey:e.ctrlKey,
- altKey:e.altKey
- };
- }
- updateSelection(x, y, ctrlKey) {
- //update selected items
- let item = this.itemAt(x,y);
- if(item) {
- var index = this.selectedItems.indexOf(item);
- // the item was already selected
- if(index < 0) {
- if(ctrlKey) {
- this.selectedItems.push(item);
- } else {
- this.selectedItems = [item];
- }
- }
- } else {
- //click in the void -> deselect everything
- this.selectedItems = [];
- }
- return item;
- }
- dispatch(evt, payload) {
- if(evt === 'mousedown') {
- // if we clicked on a handle
- this.dragAction = this.findHandleAction(payload.x, payload.y);
- if(this.dragAction == null) {
- let item = this.updateSelection(payload.x, payload.y, payload.ctrlKey);
- if(item == null) {
- // and start a selection action
- this.dragAction = new SelectAction(payload.x, payload.y, this);
- } else {
- // clicked on a selected item : start a move action
- this.dragAction = new MoveAction(payload.x, payload.y, new Group(this.selectedItems));
- }
- }
- }
- else if(evt === 'mousemove') {
- if(this.dragAction){
- this.dragAction.update(payload.x, payload.y);
- }
- }
- else if(evt === 'mouseup') {
- // end of dragging event
- if(this.dragAction != null) {
- this.dragAction.done();
- this.dragAction = null;
- }
- }
- requestAnimationFrame(this.draw.bind(this));
- }
- drawHandle(x, y) {
- const H = HANDLE_SIZE;
- const H2 = HANDLE_SIZE/2;
- this.ctx.fillStyle = 'white';
- this.ctx.strokeStyle = 'gray';
- this.ctx.fillRect(x-H2 + 0.5, y-H2 + 0.5, H, H);
- this.ctx.beginPath();
- this.ctx.rect(x-H2 + 0.5, y-H2 + 0.5, H, H);
- this.ctx.stroke();
- }
- draw() {
- this.ctx.save();
- //clear canvas
- this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
- this.ctx.fillStyle = 'white';
- this.ctx.strokeStyle='black';
- // draw items
- for(let item of this.drawables) {
- this.ctx.save();
- item.draw(this.ctx);
- this.ctx.restore();
- }
- // highlight selected items
- this.selectedItems
- .map((item) => item.bbox())
- .forEach((bbox) => {
- this.ctx.setLineDash([3]);
- this.ctx.beginPath();
- this.ctx.rect(bbox.left + 0.5, bbox.top + 0.5, bbox.width, bbox.height);
- this.ctx.stroke();
- this.ctx.setLineDash([]);
- this.drawHandle(bbox.left, bbox.top);
- this.drawHandle(bbox.left + bbox.width, bbox.top);
- this.drawHandle(bbox.left, bbox.top + bbox.height);
- this.drawHandle(bbox.left + bbox.width, bbox.top + bbox.height);
- });
- // draw the ghost of the items being resized / moved
- if(this.dragAction && this.dragAction.hasMoved()) {
- this.ctx.globalAlpha = 0.3;
- this.dragAction.drawGhost(this.ctx);
- this.ctx.globalAlpha = 1.0;
- }
- this.ctx.restore();
- }
- itemAt(x,y) {
- let candidates = this.itemsAt(x,y);
- if(candidates.length == 0) {
- return null;
- }
- candidates.sort((a,b) => b.zIndex() - a.zIndex());
- return candidates[0];
- }
- itemsAt(x, y) {
- return this.drawables.filter((item) => item.containsPoint(x,y));
- }
- findHandleAction(x,y) {
- const H2 = HANDLE_SIZE/2;
- for(let item of this.selectedItems) {
- const scanned = {
- 'topLeft' : {x:item.getX(), y:item.getY()},
- 'topRight' : {x:item.getX() + item.getWidth(), y:item.getY()},
- 'bottomLeft' : {x:item.getX(), y:item.getY() + item.getHeight()},
- 'bottomRight' : {x:item.getX() + item.getWidth(), y:item.getY() + item.getHeight()},
- };
- for(const key of Object.keys(scanned)) {
- let pos = scanned[key];
- let box = new Box({
- left : pos.x - H2,
- top : pos.y - H2,
- width : HANDLE_SIZE,
- height : HANDLE_SIZE
- });
- if(box.containsPoint(x,y)) {
- let group = new Group(this.selectedItems);
- return new ResizeAction(x, y, group, key);
- }
- }
- }
- return null;
- }
- };
- window.bodyOnLoad = () => {
- let canvas = document.getElementById('canvas');
- // make 1:1 pixel ratio
- let scale = window.devicePixelRatio;
- canvas.width = canvas.clientWidth * scale;
- canvas.height = canvas.clientHeight * scale;
- let ctx = canvas.getContext('2d');
- ctx.scale(scale, scale);
- let v = new ItemsView(ctx);
- v.drawables = [
- new Rect(100,100,100,100),
- new Rect(200,200,100, 100),
- new Circle(300,300,100),
- new Oval(400,400, 20,30)
- ];
- v.draw();
- };
- </script>
- </head>
- <body onload="bodyOnLoad()">
- <div id="container">
- <canvas id="canvas" />
- </div>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement