Flat, round, designer-friendly pseudo-3D engine for canvas & SVG
1/**
2 * Texture
3 */
4( function( root, factory ) {
5 // module definition
6 if ( typeof module == 'object' && module.exports ) {
7 // CommonJS
8 module.exports = factory(require('./vector'));
9 } else {
10 // browser global
11 var Zdog = root.Zdog;
12 Zdog.Texture = factory(Zdog.Vector);
13 }
14 }( this, function factory(Vector) {
15
16 /**
17 * Calculates the inverse of the matrix:
18 * | x1 x2 x3 |
19 * | y1 y2 y3 |
20 * | 1 1 1 |
21 */
22 function inverse(x1, y1, x2, y2, x3, y3) {
23 let tp = [
24 y2 - y3, x3 - x2, x2*y3 - x3*y2,
25 y3 - y1, x1 - x3, x3*y1 - x1*y3,
26 y1 - y2, x2 - x1, x1*y2 - x2*y1];
27 let det = tp[2] + tp[5] + tp[8];
28 return tp.map(function(x) { return x / det;});
29 }
30
31 function parsePointMap(size, map) {
32 if (!Array.isArray(map) || !map.length) {
33 map = [0, 0, size[0], size[1]];
34 }
35 if (typeof(map[0]) == "number") {
36 if (map.length < 4) {
37 let tmp = map;
38 map = [0, 0, size[0], size[1]];
39 for (let i = 0; i < tmp.length; i++) {
40 map[i] = tmp[i];
41 }
42 }
43 return [
44 new Vector({x:map[0], y:map[1], z:1}),
45 new Vector({x:map[0] + map[2], y:map[1], z:1}),
46 new Vector({x:map[0], y:map[1] + map[3], z:1})
47 ];
48 } else {
49 return [new Vector(map[0]), new Vector(map[1]), new Vector(map[2])];
50 }
51 }
52
53 var idCounter = 0;
54
55 const optionKeys = [
56 'img',
57 'linearGrad',
58 'radialGrad',
59 'colorStops',
60 'src',
61 'dst'
62 ]
63
64 /**
65 * Creates a tecture map. Possible options:
66 * img: Image object to be used as texture
67 * linearGrad: [x1, y1, x2, y2] Array defining the linear gradient
68 * radialGrad: [x0, y0, r0, x1, y1, r1] Array defining the radial gradient
69 * colorStops: [offset1, color1, offset2, color2...] Array defining the color
70 * stops for the gradient, offset must be in range [0, 1]
71 *
72 * src: <surface definition> Represents the surface for the texture. Above
73 * gradient definition should be represented in this coordinate space
74 * dst: <surface definition> Represents the surface of the object. This allows
75 * keeping the texture definition independent of the surface definition
76 *
77 * <surface definition> Can be represented in one of the following ways:
78 * [x, y, width, height] => We use 3 points top-left, top-right, bottom-left
79 * [x, y] => image/gradient size is used for width and height with the above rule
80 * [vector, vector, vector] => provided points are used
81 */
82 function Texture(options) {
83 this.id = idCounter++;
84 this.isTexture = true;
85
86 options = options || { }
87 for (var key in options ) {
88 if (optionKeys.indexOf( key ) != -1 ) {
89 this[key] = options[key];
90 }
91 }
92
93 var size;
94 if (options.img) {
95 size = [options.img.width, options.img.height];
96 } else if (options.linearGrad) {
97 size = [Math.abs(options.linearGrad[2] - options.linearGrad[0]), Math.abs(options.linearGrad[3] - options.linearGrad[1])];
98 } else if (options.radialGrad) {
99 size = [Math.abs(options.radialGrad[3] - options.radialGrad[0]), Math.abs(options.radialGrad[4] - options.radialGrad[1])];
100 } else {
101 throw "One of [img, linearGrad, radialGrad] is required";
102 }
103 if (size[0] == 0) size[0] = size[1];
104 if (size[1] == 0) size[1] = size[0];
105
106 this.src = parsePointMap(size, options.src);
107 this.dst = parsePointMap(size, options.dst);
108
109 this.srcInverse = inverse(
110 this.src[0].x, this.src[0].y,
111 this.src[1].x, this.src[1].y,
112 this.src[2].x, this.src[2].y);
113 this.p1 = new Vector();
114 this.p2 = new Vector();
115 this.p3 = new Vector();
116 this.matrix = [0, 0, 0, 0, 0, 0];
117 };
118
119 Texture.prototype.getMatrix = function() {
120 let m = this.matrix;
121 let inverse = this.srcInverse;
122 m[0] = this.p1.x * inverse[0] + this.p2.x * inverse[3] + this.p3.x * inverse[6];
123 m[1] = this.p1.y * inverse[0] + this.p2.y * inverse[3] + this.p3.y * inverse[6];
124 m[2] = this.p1.x * inverse[1] + this.p2.x * inverse[4] + this.p3.x * inverse[7];
125 m[3] = this.p1.y * inverse[1] + this.p2.y * inverse[4] + this.p3.y * inverse[7];
126 m[4] = this.p1.x * inverse[2] + this.p2.x * inverse[5] + this.p3.x * inverse[8];
127 m[5] = this.p1.y * inverse[2] + this.p2.y * inverse[5] + this.p3.y * inverse[8];
128 return m;
129 }
130
131 Texture.prototype.getCanvasFill = function(ctx) {
132 if (!this.pattern) {
133 if (this.img) {
134 this.pattern = ctx.createPattern(this.img, "repeat");
135 } else {
136 this.pattern = this.linearGrad
137 ? ctx.createLinearGradient.apply(ctx, this.linearGrad)
138 : ctx.createRadialGradient.apply(ctx, this.radialGrad);
139 if (this.colorStops) {
140 for (var i = 0; i < this.colorStops.length; i+=2) {
141 this.pattern.addColorStop(this.colorStops[i], this.colorStops[i+1]);
142 }
143 }
144 }
145 }
146 // pattern.setTransform is not supported in IE,
147 // so transform the context instead
148 ctx.transform.apply(ctx, this.getMatrix());
149 return this.pattern;
150 };
151
152 const svgURI = 'http://www.w3.org/2000/svg';
153 Texture.prototype.getSvgFill = function(svg) {
154 if (!this.svgPattern) {
155 if (this.img) {
156 this.svgPattern = document.createElementNS( svgURI, 'pattern');
157 this.svgPattern.setAttribute("width", this.img.width);
158 this.svgPattern.setAttribute("height", this.img.height);
159 this.svgPattern.setAttribute("patternUnits", "userSpaceOnUse");
160 this.attrTransform = "patternTransform";
161
162 let img = document.createElementNS( svgURI, 'image');
163 img.setAttribute("href", this.img.src);
164 this.svgPattern.appendChild(img);
165 } else {
166 var type, vals, keys;
167 if (this.linearGrad) {
168 type = "linearGradient";
169 vals = this.linearGrad;
170 keys = ["x1", "y1", "x2", "y2"]
171 } else {
172 type = "radialGradient";
173 vals = this.radialGrad;
174 keys = ["fx", "fy", "fr", "cx", "cy", "r"]
175 }
176 this.svgPattern = document.createElementNS( svgURI, type);
177 for (var i = 0; i < keys.length; i++) {
178 this.svgPattern.setAttribute(keys[i], vals[i]);
179 }
180
181 if (this.colorStops) {
182 for (var i = 0; i < this.colorStops.length; i+=2) {
183 let colorStop = document.createElementNS(svgURI, 'stop' );
184 colorStop.setAttribute("offset", this.colorStops[i]);
185 colorStop.setAttribute("style", "stop-color:" + this.colorStops[i+1]);
186 this.svgPattern.appendChild(colorStop);
187 }
188 }
189 this.svgPattern.setAttribute("gradientUnits", "userSpaceOnUse");
190 this.attrTransform = "gradientTransform";
191 }
192 this.svgPattern.setAttribute("id", "texture_" + this.id);
193 this._svgUrl = 'url(#texture_' + this.id + ')';
194
195 this.defs = document.createElementNS(svgURI, 'defs' );
196 this.defs.appendChild(this.svgPattern);
197 }
198
199 this.svgPattern.setAttribute(this.attrTransform, 'matrix(' + this.getMatrix().join(' ') + ')');
200 svg.appendChild( this.defs );
201 return this._svgUrl;
202 }
203
204 // ----- update ----- //
205 Texture.prototype.reset = function() {
206 this.p1.set(this.dst[0]);
207 this.p2.set(this.dst[1]);
208 this.p3.set(this.dst[2]);
209 };
210
211 Texture.prototype.transform = function( translation, rotation, scale ) {
212 this.p1.transform(translation, rotation, scale);
213 this.p2.transform(translation, rotation, scale);
214 this.p3.transform(translation, rotation, scale);
215 };
216
217 Texture.prototype.clone = function() {
218 return new Texture(this);
219 };
220
221 return Texture;
222} ) );