jellies-draw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <div class="buttons">
3
+ <tool-button
4
+ v-for="tool, index in tools"
5
+ :key="tool"
6
+ :action="tool"
7
+ :short-cut="(index + 1) % 10"
8
+ :is-active="currentTool === tool"
9
+ @action-applied="applyAction"
10
+ />
11
+ <input
12
+ ref="fileSelector"
13
+ class="file-selector"
14
+ type="file"
15
+ @change="uploadImage"
16
+ />
17
+ </div>
18
+ </template>
19
+ <script>
20
+ import ToolButton from './ToolButton.vue'
21
+ import Properties from './functions/properties'
22
+ import Tools from './functions/tools'
23
+ export default {
24
+ name: 'ToolButtons',
25
+ components: {
26
+ ToolButton
27
+ },
28
+ props: {
29
+ hasShortCuts: {
30
+ type: Boolean,
31
+ default: false
32
+ }
33
+ },
34
+ mounted() {
35
+ document.addEventListener('keydown', this.handleShortCutKey)
36
+ },
37
+ beforeDestroy() {
38
+ document.removeEventListener('keydown', this.handleShortCutKey)
39
+ },
40
+ data() {
41
+ return {
42
+ tools: [
43
+ 'selector',
44
+ 'rectangle',
45
+ 'ellipse',
46
+ 'arrow',
47
+ 'line',
48
+ 'pen',
49
+ 'text',
50
+ 'image',
51
+ 'eraser',
52
+ 'clear'
53
+ ]
54
+ }
55
+ },
56
+ computed: {
57
+ currentTool: {
58
+ get() {
59
+ return Properties.tool;
60
+ },
61
+ set(newTool) {
62
+ Properties.tool = newTool;
63
+ }
64
+ }
65
+ },
66
+ watch: {
67
+ currentTool(newTool) {
68
+ this.currentTool = newTool;
69
+ }
70
+ },
71
+ methods: {
72
+ applyAction(action) {
73
+ if (action === 'image') {
74
+ this.$refs.fileSelector.click()
75
+ }
76
+ if (action === 'clear') {
77
+ Tools.clear.clear()
78
+ }
79
+ this.currentTool = action
80
+ this.releaseInstantTool()
81
+ },
82
+ releaseInstantTool() {
83
+ if (this.currentTool === 'image' || this.currentTool === 'clear') {
84
+ setTimeout(() => {
85
+ this.currentTool = 'selector'
86
+ }, 300)
87
+ }
88
+ },
89
+ uploadImage(event) {
90
+ const file = event.target.files[0]
91
+ Tools.image.add(file)
92
+ event.target.value = ''
93
+ },
94
+ handleShortCutKey(event) {
95
+ if (this.hasShortCuts && this.currentTool !== 'text') {
96
+ const key = event.key
97
+ const index = parseInt(key, 10)
98
+ if (index >= 0 && index < this.tools.length) {
99
+ this.applyAction(this.tools[(index + 9) % 10])
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ </script>
106
+ <style scoped>
107
+ .buttons {
108
+ display: flex;
109
+ }
110
+ .file-selector {
111
+ display: none;
112
+ }
113
+ </style>
@@ -0,0 +1,136 @@
1
+ import Konva from 'konva';
2
+ import Transformer from './transformer';
3
+ import Clipboard from './clipboard';
4
+ import Properties from './properties';
5
+ import Tools from './tools';
6
+ import Histories from './histories';
7
+ export default {
8
+ layer: null,
9
+ stage: null,
10
+ container: null,
11
+ resizeHandler: null,
12
+ keydownHandler: null,
13
+ pointerDownHandler: null,
14
+ pointerMoveHandler: null,
15
+ pointerUpHandler: null,
16
+ clickHandler: null,
17
+ isEditingText: false,
18
+ isDrawing: false,
19
+ isRecording: false,
20
+ initialize(canvas, container) {
21
+ this.container = container
22
+ this.generateEventHandlers()
23
+ window.addEventListener('resize', this.resizeHandler);
24
+ this.stage = new Konva.Stage({
25
+ container: canvas,
26
+ width: container.offsetWidth,
27
+ height: container.offsetHeight
28
+ });
29
+ this.layer = new Konva.Layer();
30
+ this.stage.add(this.layer);
31
+ document.addEventListener('keydown', this.keydownHandler);
32
+ Transformer.initialize();
33
+ Histories.record();
34
+ this.stage.on('pointerdown', this.pointerDownHandler);
35
+ this.stage.on('pointermove', this.pointerMoveHandler);
36
+ this.stage.on('pointerup', this.pointerUpHandler);
37
+ this.stage.on('click tap', this.clickHandler);
38
+ },
39
+ generateEventHandlers() {
40
+ this.resizeHandler = this._handleResize.bind(this);
41
+ this.keydownHandler = this._handleKeydown.bind(this);
42
+ this.pointerDownHandler = this.handlePointerDown.bind(this);
43
+ this.pointerMoveHandler = this.handlePointerMove.bind(this);
44
+ this.pointerUpHandler = this.handlePointerUp.bind(this);
45
+ this.clickHandler = this.handleClick.bind(this);
46
+ },
47
+ clear() {
48
+ this.layer.destroyChildren();
49
+ Transformer.initialize();
50
+ },
51
+ destroy() {
52
+ this.stage.off('pointerdown', this.pointerDownHandler);
53
+ this.stage.off('pointermove', this.pointerMoveHandler);
54
+ this.stage.off('pointerup', this.pointerUpHandler);
55
+ this.stage.off('click tap', this.clickHandler);
56
+ document.removeEventListener('keydown', this.keydownHandler);
57
+ this.stage.clear()
58
+ this.stage = null;
59
+ window.removeEventListener('resize', this.resizeHandler);
60
+ },
61
+ _handleResize() {
62
+ if (this.stage) {
63
+ this.stage.width(this.container.offsetWidth);
64
+ this.stage.height(this.container.offsetHeight);
65
+ this.stage.batchDraw();
66
+ }
67
+ },
68
+ _handleKeydown(event) {
69
+ if (event.key === 'Escape') {
70
+ Transformer.deselectAllNodes();
71
+ } else if (event.key === 'Backspace') {
72
+ Transformer.removeSelectedNodes();
73
+ } else if (event.ctrlKey || event.metaKey) {
74
+ if (!this.isEditingText) {
75
+ this._handleKeyShortCuts(event);
76
+ }
77
+ }
78
+ },
79
+ _handleKeyShortCuts(event) {
80
+ event.preventDefault();
81
+ if (event.key === 'c') {
82
+ Clipboard.copy();
83
+ } else if (event.key === 'x') {
84
+ Clipboard.cut();
85
+ } else if (event.key === 'v') {
86
+ Clipboard.paste();
87
+ } else if (event.key === 'a') {
88
+ Transformer.selectAllNodes();
89
+ } else if (event.key === 'z' || event.key === 'Z') {
90
+ if (event.shiftKey) {
91
+ Histories.redo()
92
+ } else {
93
+ Histories.undo()
94
+ }
95
+ }
96
+ },
97
+ handlePointerDown(event) {
98
+ if (Properties.tool === 'selector') {
99
+ if (event.target !== this.stage) {
100
+ return;
101
+ }
102
+ }
103
+ Tools[Properties.tool].show(event.evt)
104
+ },
105
+ handlePointerMove(event) {
106
+ if (Properties.tool !== 'clear') {
107
+ Tools[Properties.tool].change(event.evt)
108
+ }
109
+ },
110
+ handlePointerUp(event) {
111
+ Tools[Properties.tool].finish()
112
+ if (this.isDrawing || this.isEditingText) {
113
+ Histories.record()
114
+ }
115
+ this.isDrawing = false
116
+ this.isEditingText = false
117
+ },
118
+ handleClick(event) {
119
+ const metaPressed = event.evt.shiftKey || event.evt.ctrlKey || event.evt.metaKey;
120
+ const targetNode = event.target;
121
+ if (Properties.isUsingDrawingTool) {
122
+ return;
123
+ }
124
+ if (Tools.selector.isSelecting()) {
125
+ return;
126
+ }
127
+ if (!targetNode.hasName('node')) {
128
+ return;
129
+ }
130
+ if (targetNode === this.stage) {
131
+ Transformer.deselectAllNodes();
132
+ return;
133
+ }
134
+ Transformer.selectNode(targetNode, metaPressed);
135
+ }
136
+ }
@@ -0,0 +1,32 @@
1
+ import Canvas from './canvas';
2
+ import Transformer from './transformer';
3
+ import Tools from './tools';
4
+ export default {
5
+ copiedNodes: [],
6
+ copy() {
7
+ this.copiedNodes = Transformer.copySelectedNodes();
8
+ },
9
+ cut() {
10
+ this.copy();
11
+ Transformer.removeSelectedNodes();
12
+ },
13
+ paste() {
14
+ if (this.copiedNodes) {
15
+ this.copiedNodes.forEach((node) => {
16
+ node.x(node.x() + 10);
17
+ node.y(node.y() + 10);
18
+ this._addEventsToCopiedNode(node);
19
+ Canvas.layer.add(node);
20
+ });
21
+ Transformer.selectNodes(this.copiedNodes);
22
+ this.copiedNodes = Transformer.copySelectedNodes();
23
+ }
24
+ },
25
+ _addEventsToCopiedNode(node) {
26
+ if (node.getClassName() === 'Text') {
27
+ Tools.text.bindEvents(node);
28
+ } else if (node.getClassName() === 'Arrow') {
29
+ Tools.arrow.bindEvents(node);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,53 @@
1
+ import Vue from 'vue'
2
+ import Konva from 'konva'
3
+ import Canvas from './canvas'
4
+ import Transformer from './transformer'
5
+ import Tools from './tools'
6
+ import Properties from './properties'
7
+ export default new Vue({
8
+ data() {
9
+ return {
10
+ histories: [],
11
+ historyIndex: -1
12
+ }
13
+ },
14
+ methods: {
15
+ record() {
16
+ if (Canvas.isRecording) {
17
+ return
18
+ }
19
+ Canvas.isRecording = true
20
+ this.histories.splice(this.historyIndex + 1)
21
+ this.histories.push(Canvas.layer.toJSON())
22
+ this.historyIndex++
23
+ this.$nextTick(() => {
24
+ Canvas.isRecording = false
25
+ })
26
+ },
27
+ undo() {
28
+ if (this.historyIndex > 0) {
29
+ this.historyIndex--
30
+ this.refreshNodes()
31
+ }
32
+ },
33
+ redo() {
34
+ if (this.historyIndex < this.histories.length - 1) {
35
+ this.historyIndex++
36
+ this.refreshNodes()
37
+ }
38
+ },
39
+ refreshNodes() {
40
+ Canvas.layer.destroyChildren();
41
+ const latestNodes = Konva.Node.create(this.histories[this.historyIndex]).find('.node')
42
+ latestNodes.forEach(({ attrs }) => {
43
+ if (attrs.nodeType === 'image') {
44
+ Tools[attrs.nodeType].generate(attrs)
45
+ } else {
46
+ Canvas.layer.add(Tools[attrs.nodeType].generate(attrs))
47
+ }
48
+ })
49
+ Transformer.initialize()
50
+ Properties.refreshNodesStatus()
51
+ }
52
+ }
53
+ })
@@ -0,0 +1,126 @@
1
+ import Vue from 'vue'
2
+ import Canvas from './canvas'
3
+ import Transformer from './transformer'
4
+
5
+ export default new Vue({
6
+ data() {
7
+ return {
8
+ fillColorPicked: 'transparent',
9
+ strokeColorPicked: '#000000',
10
+ fontSizeSelected: 20,
11
+ strokeWidthSelected: 4,
12
+ toolSelected: 'selector',
13
+ isTextNodesSelected: false,
14
+ isMassivelyAssigningProperties: false
15
+ }
16
+ },
17
+ computed: {
18
+ fillColor: {
19
+ get() {
20
+ return this.fillColorPicked;
21
+ },
22
+ set(newFillColor) {
23
+ this.fillColorPicked = newFillColor;
24
+ if (!this.isMassivelyAssigningProperties) {
25
+ Transformer.setPropertiesOfSelectedNodes({
26
+ fill: newFillColor
27
+ });
28
+ }
29
+ }
30
+ },
31
+ strokeColor: {
32
+ get() {
33
+ return this.strokeColorPicked;
34
+ },
35
+ set(newStrokeColor) {
36
+ this.strokeColorPicked = newStrokeColor;
37
+ if (!this.isMassivelyAssigningProperties) {
38
+ Transformer.setPropertiesOfSelectedNodes({
39
+ stroke: newStrokeColor
40
+ });
41
+ }
42
+ }
43
+ },
44
+ fontSize: {
45
+ get() {
46
+ return this.fontSizeSelected;
47
+ },
48
+ set(newFontSize) {
49
+ this.fontSizeSelected = newFontSize;
50
+ if (!this.isMassivelyAssigningProperties) {
51
+ Transformer.setPropertiesOfSelectedNodes({
52
+ fontSize: parseInt(newFontSize)
53
+ });
54
+ }
55
+ }
56
+ },
57
+ strokeWidth: {
58
+ get() {
59
+ return this.strokeWidthSelected;
60
+ },
61
+ set(newStrokeWidth) {
62
+ this.strokeWidthSelected = newStrokeWidth;
63
+ if (!this.isMassivelyAssigningProperties) {
64
+ Transformer.setPropertiesOfSelectedNodes({
65
+ strokeWidth: parseInt(newStrokeWidth)
66
+ });
67
+ }
68
+ }
69
+ },
70
+ tool: {
71
+ get() {
72
+ return this.toolSelected;
73
+ },
74
+ set(newTool) {
75
+ this.toolSelected = newTool;
76
+ }
77
+ },
78
+ isUsingText() {
79
+ return this.tool === 'text' || this.isTextNodesSelected;
80
+ },
81
+ isUsingDrawingTool() {
82
+ return (this.tool !== 'selector');
83
+ }
84
+ },
85
+ watch: {
86
+ toolSelected() {
87
+ this.refreshNodesStatus()
88
+ }
89
+ },
90
+ methods: {
91
+ bindMoveCursor(shape) {
92
+ if (this.isUsingDrawingTool) {
93
+ shape.off('mouseenter', this.handleSwitchToMoveCursor);
94
+ shape.off('mouseleave', this.handleClearMoveCursor);
95
+ } else {
96
+ shape.on('mouseenter', this.handleSwitchToMoveCursor);
97
+ shape.on('mouseleave', this.handleClearMoveCursor);
98
+ }
99
+ },
100
+ handleSwitchToMoveCursor(){
101
+ Canvas.stage.container().style.cursor = 'move';
102
+ },
103
+ handleClearMoveCursor(){
104
+ Canvas.stage.container().style.cursor = null;
105
+ },
106
+ setProperties(properties) {
107
+ this.isMassivelyAssigningProperties = true;
108
+ Object.keys(properties).forEach(property => {
109
+ this[property] = properties[property];
110
+ });
111
+ this.isMassivelyAssigningProperties = false;
112
+ },
113
+ refreshNodesStatus() {
114
+ if (Canvas.layer) {
115
+ Canvas.layer.children.forEach(shape => {
116
+ shape.draggable(!this.isUsingDrawingTool);
117
+ this.bindMoveCursor(shape);
118
+ if (this.isUsingDrawingTool || this.isUsingText) {
119
+ Transformer.deselectAllNodes();
120
+ this.handleClearMoveCursor();
121
+ }
122
+ });
123
+ }
124
+ }
125
+ }
126
+ })
@@ -0,0 +1,70 @@
1
+ import Konva from 'konva'
2
+ import Properties from '../properties'
3
+ import Canvas from '../canvas'
4
+ import Histories from '../histories'
5
+ export default {
6
+ temporalShape: null,
7
+ startX: 0,
8
+ startY: 0,
9
+ show({ offsetX, offsetY }) {
10
+ Canvas.isDrawing = true;
11
+ this.startX = offsetX
12
+ this.startY = offsetY
13
+ const strokeWidth = Properties.strokeWidth
14
+ const pointerWidth = Math.pow(Properties.strokeWidth, 2) / 2
15
+ const pointerLength = Math.pow(Properties.strokeWidth, 2) / 2
16
+ const stroke = Properties.strokeColor
17
+ const newArrow = this.generate({
18
+ points: [offsetX, offsetY, offsetX, offsetY],
19
+ stroke,
20
+ strokeWidth,
21
+ pointerWidth,
22
+ pointerLength
23
+ })
24
+ this.temporalShape = newArrow
25
+ Canvas.layer.add(this.temporalShape)
26
+ },
27
+ change({ offsetX, offsetY }) {
28
+ if (Canvas.isDrawing) {
29
+ this.temporalShape.points([this.startX, this.startY, offsetX, offsetY]);
30
+ }
31
+ },
32
+ finish() {
33
+ Properties.tool = 'selector'
34
+ this.temporalShape = null
35
+ },
36
+ bindEvents(newArrow) {
37
+ newArrow.off('transform')
38
+ newArrow.off('transformend')
39
+ newArrow.off('dragend')
40
+ newArrow.on('transform', () => {
41
+ const pointerWidth = Math.pow(newArrow.strokeWidth(), 2) / 2
42
+ const pointerLength = Math.pow(newArrow.strokeWidth(), 2) / 2
43
+ const transformScale = newArrow.getAbsoluteScale().x
44
+ newArrow.pointerWidth(pointerWidth / transformScale);
45
+ newArrow.pointerLength(pointerLength / transformScale);
46
+ })
47
+ newArrow.on('transformend', Histories.record)
48
+ newArrow.on('dragend', Histories.record)
49
+ },
50
+ generate({ x, y, scaleX, scaleY, points, stroke, strokeWidth, pointerWidth, pointerLength }) {
51
+ const newArrow = new Konva.Arrow({
52
+ x,
53
+ y,
54
+ scaleX,
55
+ scaleY,
56
+ points,
57
+ stroke,
58
+ strokeWidth,
59
+ pointerWidth,
60
+ pointerLength,
61
+ name: 'node',
62
+ nodeType: 'arrow',
63
+ lineCap: 'round',
64
+ fillEnabled: false,
65
+ strokeScaleEnabled: false
66
+ })
67
+ this.bindEvents(newArrow)
68
+ return newArrow
69
+ }
70
+ }
@@ -0,0 +1,8 @@
1
+ import Canvas from '../canvas'
2
+ import Histories from '../histories'
3
+ export default {
4
+ clear() {
5
+ Canvas.clear()
6
+ Histories.record()
7
+ }
8
+ }
@@ -0,0 +1,73 @@
1
+ import Konva from 'konva'
2
+ import Properties from '../properties'
3
+ import Canvas from '../canvas'
4
+ import Histories from '../histories'
5
+ export default {
6
+ temporalShape: null,
7
+ startX: 0,
8
+ startY: 0,
9
+ show({ offsetX, offsetY }) {
10
+ Canvas.isDrawing = true;
11
+ this.startX = offsetX
12
+ this.startY = offsetY
13
+ const fill = Properties.fillColor
14
+ const stroke = Properties.strokeColor
15
+ const strokeWidth = Properties.strokeWidth
16
+ const newEllipse = this.generate({
17
+ x: offsetX,
18
+ y: offsetY,
19
+ radiusX: 1,
20
+ radiusY: 1,
21
+ fill,
22
+ stroke,
23
+ strokeWidth
24
+ })
25
+ this._checkEllipseAndChangeToVertex(newEllipse)
26
+ this.temporalShape = newEllipse
27
+ Canvas.layer.add(this.temporalShape)
28
+ },
29
+ change({ offsetX, offsetY }) {
30
+ if (Canvas.isDrawing) {
31
+ this.temporalShape.x((offsetX + this.startX) / 2);
32
+ this.temporalShape.y((offsetY + this.startY) / 2);
33
+ this.temporalShape.radiusX(Math.abs(offsetX - this.startX) / 2);
34
+ this.temporalShape.radiusY(Math.abs(offsetY - this.startY) / 2);
35
+ }
36
+ },
37
+ finish() {
38
+ Properties.tool = 'selector'
39
+ this.temporalShape = null
40
+ },
41
+ _checkEllipseAndChangeToVertex(newEllipse) {
42
+ setTimeout(() => {
43
+ if (!this.temporalShape && newEllipse.radiusX() < 5 && newEllipse.radiusY() < 5) {
44
+ newEllipse.radiusX(15)
45
+ newEllipse.radiusY(15)
46
+ }
47
+ }, 200)
48
+ },
49
+ generate({ x, y, radiusX, radiusY, scaleX, scaleY, fill, stroke, strokeWidth }) {
50
+ const newEllipse = new Konva.Ellipse({
51
+ x,
52
+ y,
53
+ radiusX,
54
+ radiusY,
55
+ scaleX,
56
+ scaleY,
57
+ fill,
58
+ stroke,
59
+ strokeWidth,
60
+ name: 'node',
61
+ nodeType: 'ellipse',
62
+ strokeScaleEnabled: false
63
+ })
64
+ this.bindEvents(newEllipse)
65
+ return newEllipse
66
+ },
67
+ bindEvents(newEllipse) {
68
+ newEllipse.off('transformend')
69
+ newEllipse.off('dragend')
70
+ newEllipse.on('transformend', Histories.record)
71
+ newEllipse.on('dragend', Histories.record)
72
+ }
73
+ }
@@ -0,0 +1,51 @@
1
+ import Konva from 'konva'
2
+ import Properties from '../properties'
3
+ import Canvas from '../canvas'
4
+ export default {
5
+ temporalCurveTrajectory: null,
6
+ temporalShape: null,
7
+ startX: 0,
8
+ startY: 0,
9
+ show({ offsetX, offsetY }) {
10
+ Canvas.isDrawing = true;
11
+ this.startX = offsetX
12
+ this.startY = offsetY
13
+ this.temporalCurveTrajectory = null
14
+ const strokeWidth = Properties.strokeWidth
15
+ const newCurve = new Konva.Line({
16
+ points: [offsetX, offsetY, offsetX, offsetY],
17
+ stroke: 'transparent',
18
+ strokeWidth,
19
+ name: 'node',
20
+ bezier: true,
21
+ lineCap: 'round',
22
+ strokeScaleEnabled: false
23
+ });
24
+ this.temporalShape = newCurve
25
+ Canvas.layer.add(this.temporalShape);
26
+ },
27
+ change({ offsetX, offsetY }) {
28
+ if (Canvas.isDrawing) {
29
+ if (!this.temporalCurveTrajectory) {
30
+ this.temporalCurveTrajectory = [this.startX, this.startY]
31
+ }
32
+ this.temporalCurveTrajectory.push(offsetX, offsetY)
33
+ this.temporalShape.points(this.temporalCurveTrajectory);
34
+ Canvas.stage.find('.node')
35
+ .filter(node => this._hasIntersectionOnEraserCurve(node))
36
+ .forEach(node => node.opacity(0.1));
37
+ }
38
+ },
39
+ finish() {
40
+ Canvas.stage.find('.node')
41
+ .filter(node => this._hasIntersectionOnEraserCurve(node))
42
+ .forEach(node => node.remove())
43
+ this.temporalShape = null
44
+ },
45
+ _hasIntersectionOnEraserCurve(node) {
46
+ return Konva.Util.haveIntersection(
47
+ this.temporalShape.getClientRect(),
48
+ node.getClientRect()
49
+ );
50
+ }
51
+ }