Source Layer javaScript tool example

For a final new javaScript example for this month at least I made what might prove to be the first of several tool type project examples. What I mean by this is that I might often want to create some kind of project where the aim is to create some kind of resource such as a sprite sheet, world map, or maybe just some kind of image asset actually. I do not care so take the time to create a full blown image editor from the ground up mind you when it comes to that I have come to enjoy just using GIMP and moving on with my life. However often it might make sense to create some kind of custom image editor where I can open up a image file that I do not want to use directly as part of the image project, but to just serve as a source for a drawing that I will create in another canvas layer on top of this source layer.

So then this javaScript tool example will be a simple art program, however that main focus here is to create a kind of simple module that will be used to create an mutate a kind of source object that will be used in this tool, but can easily be used in other tools without having to change much to the code of the module.

1 - The source layer module

The main event of this javaScript example is then the source layer module that I made, later in this post I will be going over some additional code that will have to do with making use of this to create a basic pain program. There are two main methods of interest with this module one of which is the create method that will create and return a source layer object and the other has to do with creating a user interface that will be used to mutate such an object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
var sourceLayer = (function(){
// public api
var api = {
ver: 'r3'
};
// hard coded values
var ON_IMAGE_LOAD = function(source){};
var ON_UPDATE = function(source){};
var UI_HTML = '<span>Background:</span><br><br>' +
'image: <input id=\"ui-background-image\" type=\"file\"><br><br>' +
'mode: <select id=\"input-background-mode\">' +
'<option value=\"center\">Center</option>' +
'<option value=\"custom\">Custom</option>' +
'<option value=\"stretch\">Stretch</option>' +
'</select><br><br>' +
'<div id="bgui-zoom" ><input id=\"ui-background-zoom\" type=\"range\" value=\"1\" min=\"0\" max=\"4\" step=\"0.05\">' +
'<span>Zoom</span><br></div>' +
'<div id="bgui-rotation" >'+
'<input id=\"ui-background-rotation\" type=\"range\" value=\"0\" min=\"0\" max=\"1\" step=\"0.01\">' +
'<span>Rotation</span><br>' +
'</div>' +
'<div id="bgui-pos" >dx: <input id=\"ui-background-dx\" type=\"text\" size="4"> '+
'dy: <input id=\"ui-background-dy\" type=\"text\" size="4"> <br></div>' +
'<div id="bgui-size" >dw: <input id=\"ui-background-dw\" type=\"text\" size="4"> ' +
'dh: <input id=\"ui-background-dh\" type=\"text\" size="4"> <br></div>';
// back ground modes
var MODES = {};
// use full image mode helper
var useFullImageSource = function(source){
source.sx = 0;
source.sy = 0;
if(source.image){
source.sw = source.image.width;
source.sh = source.image.height;
}
};
var useCenterPos = function(source){
source.dx = source.canvas.width / 2;
source.dy = source.canvas.height / 2;
};
var useSourceSize = function(source){
source.dw = source.sw;
source.dh = source.sh;
}
// center mode
MODES.center = {
controls: ['zoom', 'rotation'],
init: function(source){
useFullImageSource(source);
useCenterPos(source);
useSourceSize(source);
},
update: function(source){}
};
// custom mode
MODES.custom = {
controls: ['zoom', 'rotation', 'pos', 'size'],
init: function(source){
useFullImageSource(source);
useCenterPos(source);
useSourceSize(source);
},
update: function(source){}
};
// stretch mode
MODES.stretch = {
controls: [],
init: function(source){
useFullImageSource(source);
useCenterPos(source);
// set destanation width and height to canvas width and height
source.dw = source.canvas.width;
source.dh = source.canvas.width;
// zoom is not used and should always be 1
source.zoom = 1;
},
update: function(source){
source.zoom = 1;
}
};
// get element helper
var get = function(q){
return document.querySelector(q);
};
// resolve a string to an element object, or just return what should be a element object
var resolveElRef = function(elRef){
if(typeof elRef === 'object' && elRef != null){
return elRef
}
if(typeof elRef === 'string'){
return document.querySelector(elRef);
}
return null
};
// draw place holder image when no image is loaded
var drawPlaceHolder = function(ctx, x, y, w, h){
ctx.fillStyle = 'black';
ctx.strokeStyle = 'white';
ctx.beginPath();
ctx.rect(x ,y, w, h);
ctx.fill();
ctx.stroke();
ctx.stroke();
};
// draw the current state of a source object to the context of the source object
var draw = function(source){
var canvas = source.canvas,
ctx = source.ctx;
// clear source layer
ctx.clearRect(-1, -1 , canvas.width + 2, canvas.height + 2);
// draw source image to layer with current settings
ctx.save();
ctx.translate(source.dx, source.dy);
ctx.rotate(source.radian);
var w = source.dw * source.zoom,
h = source.dh * source.zoom,
x = w / 2 * -1,
y = h / 2 * -1;
if(source.image){
ctx.drawImage(source.image, source.sx, source.sy, source.sw, source.sh, x, y, w, h);
}else{
drawPlaceHolder(ctx, x, y, w, h);
}
ctx.restore();
};
// update a source object
var update = function(source){
var modeObj = MODES[source.mode];
if(source.image){
modeObj.update(source);
}
};
// main method used to create a source object
api.create = function(opt){
opt = opt || {};
var source = {
mode: 'center',
canvas: null,
ctx: null,
zoom: 1,
radian: 0,
image: null,
sx: 0, sy: 0, sw: 100, sh: 100, dx: 0, dy: 0, dw: 100, dh: 100,
onImageLoad: opt.onImageLoad || ON_IMAGE_LOAD,
onUpdate: opt.onUpdate || ON_UPDATE
};
var canvas = source.canvas = resolveElRef(opt.canvas);
if(canvas){
canvas.width = opt.width;
canvas.height = opt.height;
source.ctx = canvas.getContext('2d');
// values for placeholder
source.dx = canvas.width / 2;
source.dy = canvas.height / 2;
source.dw = 320;
source.dh = 240;
}
// call init foe the current mode, for first time
var modeObj = MODES[source.mode];
modeObj.init.call(source, source);
// draw and return
draw(source);
return source;
};
// for each control helper
var forEachControl = function(source, el, ifOn, ifOff){
var modeObj = MODES[source.mode];
['zoom', 'rotation', 'pos', 'size'].forEach(function(key){
var controlEl = el.querySelector('#bgui-' + key);
if(modeObj.controls.some(function(modeKey){
return key === modeKey;
})){
ifOn(source, controlEl, el, key);
}else{
ifOff(source, controlEl, el, key);
}
});
};
// display controls for just the current mode
var displayControlsForMode = function(source, el){
forEachControl(source, el,
function(source, controlEl){
controlEl.style.visibility = 'visible';
},
function(source, controlEl){
controlEl.style.visibility = 'hidden';
}
);
};
// update control values to source object
var UpdateControlValuesForMode = function(source, el){
get('#ui-background-zoom').value = source.zoom;
get('#ui-background-rotation').value = source.radian / (Math.PI * 2)
get('#ui-background-dx').value = source.dx;
get('#ui-background-dy').value = source.dy;
get('#ui-background-dw').value = source.dw;
get('#ui-background-dh').value = source.dh;
};
// create a text input hander for props like dx dy dw and dh
var createTextInputHander = function(source, el, key){
return function(e){
source[key] = e.target.value;
update(source);
draw(source);
displayControlsForMode(source, el);
UpdateControlValuesForMode(source, el);
};
};
// create an HTML User Interface for the given source object and append it to the given mount point
api.createSourceUI = function(source, mountEl){
var el = resolveElRef(mountEl);
el.innerHTML = UI_HTML;
// handlers
sourceLayer.appendImageHandler(source, '#ui-background-image');
sourceLayer.appendZoomHandler(source, '#ui-background-zoom');
sourceLayer.appendRotationHandler(source, '#ui-background-rotation');
// change mode
get('#input-background-mode').addEventListener('change', function(e){
source.mode = e.target.value;
var modeObj = MODES[source.mode];
modeObj.init.call(source, source);
update(source);
draw(source);
displayControlsForMode(source, el);
UpdateControlValuesForMode(source, el);
});
get('#ui-background-dx').addEventListener('input', createTextInputHander(source, el, 'dx'));
get('#ui-background-dy').addEventListener('input', createTextInputHander(source, el, 'dy'));
get('#ui-background-dw').addEventListener('input', createTextInputHander(source, el, 'dw'));
get('#ui-background-dh').addEventListener('input', createTextInputHander(source, el, 'dh'));
displayControlsForMode(source, el);
UpdateControlValuesForMode(source);
};
// append image hander
api.appendImageHandler = function(source, fileEl){
var fileEl = resolveElRef(fileEl);
fileEl.addEventListener('change', function(e){
var files = e.target.files,
file = files[0];
var reader = new FileReader();
reader.addEventListener('load', function () {
var img = source.image = new Image();
img.src = reader.result;
img.addEventListener('load', function(){
source.onImageLoad.call(source, source);
var modeObj = MODES[source.mode];
modeObj.init.call(source, source);
update(source);
draw(source);
UpdateControlValuesForMode(source);
source.onUpdate.call(source, source);
});
}, false);
if (file) {
reader.readAsDataURL(file);
}
});
};
// zoom hander
api.appendZoomHandler = function(source, fileEl){
var fileEl = resolveElRef(fileEl);
fileEl.addEventListener('input', function(e){
source.zoom = e.target.value
draw(source);
source.onUpdate.call(source, source);
});
};
// rotation
api.appendRotationHandler = function(source, fileEl){
var fileEl = resolveElRef(fileEl);
fileEl.addEventListener('input', function(e){
source.radian = Math.PI * 2 * (parseFloat(e.target.value) / 1);
draw(source);
source.onUpdate.call(source, source);
});
};
// return the public API
return api;
}());

2 - Demo app that is a basic art program

Now that I have the source layer module worked out I will want to make a demo the makes use of this source layer module to create an over all tool. For this javaScript example the over all tool is a basic art program, and when it comes to this I do not care to create some kind of full blown image manipulation program of course, just something with a very crude set of features that I would want to start something that I would then continue to work on in such a program.

2.1 - The main javaScript file

For this over all example I just have all the additional javaScrit code that I am using in a single additional javaScript file apart from the source layer module that I include in the hard coded html.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
var get = function(q){
return document.querySelector(q);
};
// source layer set up
var source = sourceLayer.create({
canvas: '#canvas-source',
width: 640, height: 480,
onUpdate: function(source){
}
});
sourceLayer.createSourceUI(source, '#ui-background');
// out
get('#ui-out').innerText = 'version: ' + sourceLayer.ver;
// draw layer
var canvas = get('#canvas-draw'),
ctx = canvas.getContext('2d');
canvas.width = 640;
canvas.height = 480;
var sm = {
canvas: canvas,
ctx: ctx,
down: false,
size: 0.5, // 0.25 to 20 with a 0.25 step
tool: 'brush',
color: '#000000'
};
var paintAt = function(sm, pos){
var ctx = sm.ctx;
if(sm.tool === 'brush'){
ctx.beginPath();
ctx.fillStyle = sm.color;
ctx.arc(pos.x, pos.y, sm.size, 0, Math.PI * 2);
ctx.fill();
}
if(sm.tool === 'eraser'){
ctx.save();
ctx.beginPath();
ctx.arc(pos.x, pos.y, sm.size, 0, Math.PI * 2);
ctx.clip();
ctx.clearRect(pos.x - sm.size, pos.y - sm.size, sm.size * 2, sm.size * 2);
ctx.restore();
}
};
// get position helper
var getPos = function(e){
var bx = e.target.getBoundingClientRect(),
pos = {};
if(e.touches){
pos.x = e.touches[0].clientX;
pos.y = e.touches[0].clientY;
}else{
pos.x = e.clientX;
pos.y = e.clientY;
}
pos.x = pos.x - bx.left;
pos.y = pos.y - bx.top;
return pos;
};
var pointerDown = function(e){
sm.down = true;
paintAt(sm, getPos(e));
};
var pointerMove = function(e){
if(sm.down){
paintAt(sm, getPos(e));
}
};
var pointerUp = function(e){
sm.down = false;
};
var pointerOut = function(e){
sm.down = false;
};
canvas.addEventListener('mousedown', pointerDown);
canvas.addEventListener('mousemove', pointerMove);
canvas.addEventListener('mouseup', pointerUp);
canvas.addEventListener('touchstart', pointerDown);
canvas.addEventListener('touchmove', pointerMove);
canvas.addEventListener('touchend', pointerUp);
canvas.addEventListener('pointerout', pointerOut);
// clear button
get('#ui-draw-clear').addEventListener('click', function(){
sm.ctx.clearRect(-1, -1, sm.canvas.width + 2, sm.canvas.height + 2);
});
// tool select
get('#ui-draw-tool').addEventListener('input', function(e){
console.log(e.target.value);
sm.tool = e.target.value;
});
// color select
get('#ui-draw-color').addEventListener('input', function(e){
sm.color = e.target.value;
});
sm.color = get('#ui-draw-color').value;
// size select
var sizeUpdate = function(){
var size = parseFloat( get('#ui-draw-size').value );
sm.size = size;
get('#ui-draw-size-disp').innerText = size;
};
get('#ui-draw-size').addEventListener('input', function(e){
sizeUpdate();
});
sizeUpdate();

2.2 - The html file

I then have some html that will work with the main javaScript file as well as of course the source layer module that I made for this over all tool. For this example I went with having canvas elements hard coded into the html rather than creating them with javaScript.

All of the html that has to do with the drawing program is hard coded into this html, while the html that has to do with the source layer is created with a public method of the source layer module in the main javaScript file. I could not make up my mind one way or the other when it comes to that, maybe in some ways it would be better to just do everything with hard coded html actually. However in any case that aspect of the over all structure of this was not the main focus for me, that was just simply creating a program that works the way that I had in mind and when it comes to that it is working the way I want to it from an end users perspective.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<html>
<head>
<title>Source Layer</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- canvas -->
<div id="area-canvas">
<canvas id="canvas-source" class="canvas-layer"></canvas>
<canvas id="canvas-draw" class="canvas-layer"></canvas>
</div>
<!-- UI -->
<!-- out -->
<div id="ui-out" class="ui">
</div>
<!-- background -->
<div id="ui-background" class="ui">
</div>
<div id="ui-draw" class="ui">
<span>Draw:</span><br><br>
<input id="ui-draw-clear" type="button" value="clear"><br>
<select id="ui-draw-tool">
<option value="brush">Brush</option>
<option value="eraser">Eraser</option>
</select><br>
<input id="ui-draw-color" type="color" value="#000000"><br>
<input id="ui-draw-size" type="range" value="3.00" min="0.25" max="20" step="0.25">
<span id="ui-draw-size-disp"></span><br>
</div>
<script src="js/source-layer.js"></script>
<script src="js/main.js"></script>
</body>
</html>

Another thing that I did for this example is worked out at least a little css as well that I am linking to from the head of the document. So I think that I should take a moment to have a quick section on that then.

2.3 - The css for this example

I am also using at least a little additional css for this in an external css file that I also like to from the index.html file as I do with the javaScript files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.ui{
display: inline-block;
float:left;
background:gray;
margin:10px;
padding:10px;
}
#area-canvas{
display: inline-block;
float:left;
background:gray;
padding:0px;
width: 680px;
height: 520px;
}
#ui-draw{
min-width: 200px;
}
.canvas-layer{
margin: 20px;
position:absolute;
outline: 1px solid #000000;
width: 640px;
height: 480px;
}

3 - Conclusion

For this example I just wanted to make a simple source layer module as a way to pull a basic feature out of another javaScript prototype that I was working on that had to do with a source layer. So in other words I was working on another project that is like this one only it was another kind of drawing program that has to do with creating a set of points. I might get around to writing a post on that one to at some point sooner or later, but I would like to do some more work on it before doing so, and I think that I will be wanting to work what I have made for this example into that one when it comes to making additional revisions of it.

This example is also a kind of exercise for what it is that I intend to get into next month when it comes to the subject of writing desktop like software, and basic tools using javaScript and various web language tools. I have been putting off getting into election.js long enough I think and from what I have been gathering that is a great tool for working out such things when it comes to having the property binaries in a single package so that it is assured that everything that is needed will be in a single package. Anyway I do not want to write about it to much here as that is something that is off topic for this collection of JavaScript posts in which I am typicality doing everything from the ground up.

Although I might not use this art program that much, I ma use a great deal of what I worked out here might be used in additional future projects actually beyond that other javaScript example prototype that I just mentioned. I do have an idea rattling around for yet another javaScript art program but I am thinking that it will be a threejs example actually rather than a vanilla javaScript example like with this post, So I think I might also be expanding and editing my posts on the topic of threejs next month. In fact that is the main reason why I called this example tool-source-layer-2d as I think that I would also like to have something like this only it will also work with dae files. The dae file standard is the default standard that is used with blender, which is of course a popular 3d modeling program.