diff --git a/demos/textures/dice.png b/demos/textures/dice.png
new file mode 100644
index 0000000..9fea9b9
Binary files /dev/null and b/demos/textures/dice.png differ
diff --git a/demos/textures/index.html b/demos/textures/index.html
new file mode 100644
index 0000000..a7c2ebc
--- /dev/null
+++ b/demos/textures/index.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ Textures
+
+
+
+
+
+
+
+
Canvas
+
+
+
+
Gradient
+
+
+
+
+
Box
+
+
+
+
+
Tetrahedron
+
+
+
+
+
+
SVG
+
+
+
+
Gradient
+
+
+
+
+
Box
+
+
+
+
+
Tetrahedron
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/textures/tetrahedron.png b/demos/textures/tetrahedron.png
new file mode 100644
index 0000000..4c83541
Binary files /dev/null and b/demos/textures/tetrahedron.png differ
diff --git a/demos/textures/textures.js b/demos/textures/textures.js
new file mode 100644
index 0000000..3ffa8de
--- /dev/null
+++ b/demos/textures/textures.js
@@ -0,0 +1,119 @@
+function loadImage(url) {
+ return new Promise(r => { let i = new Image(); i.onload = (() => r(i)); i.src = url; });
+}
+
+function createIllustration(selector) {
+ // ----- variables ----- //
+ var isSpinning = true;
+
+ // ----- model ----- //
+ var illo = new Zdog.Illustration({
+ element: selector,
+ dragRotate: true,
+ zoom: 1,
+ onDragStart: function() {
+ isSpinning = false;
+ },
+ });
+
+ // ----- animate ----- //
+ function animate() {
+ illo.rotate.y += isSpinning ? 0.03 : 0;
+ illo.updateRenderGraph();
+ requestAnimationFrame( animate );
+ }
+ animate();
+ return illo;
+}
+
+function loadForBothRenderer(val, callback) {
+ callback("", val);
+ callback("_svg", val);
+}
+
+// Coin
+loadForBothRenderer(0, suffix => {
+ new Zdog.Cylinder({
+ addTo: createIllustration("#illo_coin" + suffix),
+ diameter: 200,
+ length: 10,
+ color: "#000",
+ backface: new Zdog.Texture({
+ linearGrad:[0, 0, 100, 0],
+ colorStops:[0, '#833ab4', .5, '#fd1d1d', 1, '#fcb045'],
+ dst: [-100, -100, 200, 200],
+ src:[0, 0, 100, 100]}),
+ frontFace: new Zdog.Texture({
+ radialGrad:[50, 50, 0, 50, 50, 50],
+ colorStops:[0, '#3f5efb', .25, '#b471a3', .5, '#adb753', .75, '#5fd0d8', 1, '#fc466b'],
+ dst: [-100, -100, 200, 200],
+ src:[0, 0, 100, 100]}),
+ stroke: false,
+ });
+});
+
+loadImage("dice.png").then(img => loadForBothRenderer(img, (suffix, img) => {
+ new Zdog.Box({
+ addTo: createIllustration("#illo_box" + suffix),
+ width: 120,
+ height: 120,
+ depth: 120,
+ stroke: false,
+ color: '#F00',
+ frontFace: new Zdog.Texture({img: img, src:[0, 0, 200, 200], dst: [-60, -60, 120, 120]}),
+ rearFace: new Zdog.Texture({img: img, src:[0, 200, 200, 200], dst: [-60, -60, 120, 120]}),
+ leftFace: new Zdog.Texture({img: img, src:[200, 0, 200, 200], dst: [-60, -60, 120, 120]}),
+ rightFace: new Zdog.Texture({img: img, src:[200, 200, 200, 200], dst: [-60, -60, 120, 120]}),
+ topFace: new Zdog.Texture({img: img, src:[400, 0, 200, 200], dst: [-60, -60, 120, 120]}),
+ bottomFace: new Zdog.Texture({img: img, src:[400, 200, 200, 200], dst: [-60, -60, 120, 120]}),
+ });
+}));
+
+
+// Tetrahedron
+loadImage("tetrahedron.png").then(img => loadForBothRenderer(img, (suffix, img) => {
+ var tetrahedron = new Zdog.Anchor({
+ addTo: createIllustration("#illo_tetrahedron" + suffix),
+ translate: { x: 0, y: 0 },
+ });
+
+ var radius = 80;
+ var deg_120 = Zdog.TAU / 3;
+
+ var depthFactor = radius * Math.sqrt(6) * 3 / 2;
+ var r = -depthFactor / 12;
+
+ var p1 = {x: 0, y : radius, z: r};
+ var p2 = {x: radius * Math.sin(deg_120), y : radius * Math.cos(deg_120), z: r};
+ var p3 = {x: radius * Math.sin(2 * deg_120), y : radius * Math.cos(2 * deg_120), z: r}
+ var p4 = {x: 0, y: 0, z: depthFactor / 4};
+
+ new Zdog.Shape({
+ path: [p1, p2, p3, p1],
+ addTo: tetrahedron,
+ stroke: 1,
+ color: new Zdog.Texture({img: img, src:[{x:10, y: 210}, {x:210, y: 210}, {x:110, y: 36.8}], dst: [p1, p2, p3]}),
+ fill: true,
+ });
+ new Zdog.Shape({
+ path: [p1, p2, p4, p1],
+ addTo: tetrahedron,
+ stroke: 1,
+ color: new Zdog.Texture({img: img, src:[{x:210, y: 210}, {x:410, y: 210}, {x:310, y: 36.8}], dst: [p1, p2, p4]}),
+ fill: true,
+ });
+ new Zdog.Shape({
+ path: [p1, p4, p3, p1],
+ addTo: tetrahedron,
+ stroke: 1,
+ color: new Zdog.Texture({img: img, src:[{x:410, y: 210}, {x:610, y: 210}, {x:510, y: 36.8}], dst: [p1, p4, p3]}),
+ fill: true,
+ });
+ new Zdog.Shape({
+ path: [p4, p2, p3, p4],
+ addTo: tetrahedron,
+ stroke: 1,
+ color: new Zdog.Texture({img: img, src:[{x:610, y: 210}, {x:810, y: 210}, {x:710, y: 36.8}], dst: [p4, p2, p3]}),
+ fill: true,
+ });
+}));
diff --git a/js/boilerplate.js b/js/boilerplate.js
index fd79612..c912d14 100644
--- a/js/boilerplate.js
+++ b/js/boilerplate.js
@@ -70,6 +70,13 @@ Zdog.easeInOut = function( alpha, power ) {
return isFirstHalf ? curve : 1 - curve;
};
+Zdog.isColor = function(value) {
+ return (typeof value == 'string') || (value && value.isTexture);
+}
+Zdog.cloneColor = function(value) {
+ return (value && value.clone) ? value.clone() : value;
+}
+
return Zdog;
} ) );
diff --git a/js/box.js b/js/box.js
index d25b802..973811b 100644
--- a/js/box.js
+++ b/js/box.js
@@ -87,7 +87,11 @@ Box.prototype.setFace = function( faceName, value ) {
}
// update & add face
var options = this.getFaceOptions( faceName );
- options.color = typeof value == 'string' ? value : this.color;
+ if (utils.isColor(value)) {
+ options.color = value;
+ } else {
+ options.color = utils.cloneColor(this.color);
+ }
if ( rect ) {
// update previous
diff --git a/js/canvas-renderer.js b/js/canvas-renderer.js
index b10ef7f..6cbbc05 100644
--- a/js/canvas-renderer.js
+++ b/js/canvas-renderer.js
@@ -51,17 +51,31 @@ CanvasRenderer.stroke = function( ctx, elem, isStroke, color, lineWidth ) {
if ( !isStroke ) {
return;
}
- ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
- ctx.stroke();
+ if (color && color.getCanvasFill) {
+ ctx.save();
+ ctx.strokeStyle = color.getCanvasFill(ctx);
+ ctx.stroke();
+ ctx.restore();
+ } else {
+ ctx.strokeStyle = color;
+ ctx.stroke();
+ }
};
CanvasRenderer.fill = function( ctx, elem, isFill, color ) {
if ( !isFill ) {
return;
}
- ctx.fillStyle = color;
- ctx.fill();
+ if (color && color.getCanvasFill) {
+ ctx.save();
+ ctx.fillStyle = color.getCanvasFill(ctx);
+ ctx.fill();
+ ctx.restore();
+ } else {
+ ctx.fillStyle = color;
+ ctx.fill();
+ }
};
CanvasRenderer.end = function() {};
diff --git a/js/cylinder.js b/js/cylinder.js
index f424106..c327dbb 100644
--- a/js/cylinder.js
+++ b/js/cylinder.js
@@ -120,14 +120,14 @@ Cylinder.prototype.create = function( /* options */) {
color: this.color,
stroke: this.stroke,
fill: this.fill,
- backface: this.frontFace || baseColor,
+ backface: utils.cloneColor(this.frontFace || baseColor),
visible: this.visible,
});
// back outside base
this.rearBase = this.group.rearBase = this.frontBase.copy({
translate: { z: -baseZ },
rotate: { y: 0 },
- backface: baseColor,
+ backface: utils.cloneColor(baseColor),
});
};
diff --git a/js/index.js b/js/index.js
index 8bed0d1..7aa15cb 100644
--- a/js/index.js
+++ b/js/index.js
@@ -24,7 +24,8 @@
require('./hemisphere'),
require('./cylinder'),
require('./cone'),
- require('./box')
+ require('./box'),
+ require('./texture')
);
} else if ( typeof define == 'function' && define.amd ) {
/* globals define */ // AMD
@@ -33,7 +34,7 @@
/* eslint-disable max-params */
} )( this, function factory( Zdog, CanvasRenderer, SvgRenderer, Vector, Anchor,
Dragger, Illustration, PathCommand, Shape, Group, Rect, RoundedRect,
- Ellipse, Polygon, Hemisphere, Cylinder, Cone, Box ) {
+ Ellipse, Polygon, Hemisphere, Cylinder, Cone, Box, Texture ) {
/* eslint-enable max-params */
Zdog.CanvasRenderer = CanvasRenderer;
@@ -53,6 +54,7 @@
Zdog.Cylinder = Cylinder;
Zdog.Cone = Cone;
Zdog.Box = Box;
+ Zdog.Texture = Texture;
return Zdog;
} );
diff --git a/js/shape.js b/js/shape.js
index 59dce32..a58e6a2 100644
--- a/js/shape.js
+++ b/js/shape.js
@@ -91,6 +91,13 @@ Shape.prototype.reset = function() {
this.pathCommands.forEach( function( command ) {
command.reset();
} );
+
+ if (this.backface && this.backface.reset) {
+ this.backface.reset();
+ }
+ if (this.color && this.color.reset) {
+ this.color.reset();
+ }
};
Shape.prototype.transform = function( translation, rotation, scale ) {
@@ -106,6 +113,12 @@ Shape.prototype.transform = function( translation, rotation, scale ) {
this.children.forEach( function( child ) {
child.transform( translation, rotation, scale );
} );
+ if (this.backface && this.backface.transform) {
+ this.backface.transform( translation, rotation, scale );
+ }
+ if (this.color && this.color.transform) {
+ this.color.transform( translation, rotation, scale );
+ }
};
Shape.prototype.updateSortValue = function() {
@@ -153,17 +166,16 @@ Shape.prototype.render = function( ctx, renderer ) {
var TAU = utils.TAU;
// Safari does not render lines with no size, have to render circle instead
-Shape.prototype.renderCanvasDot = function( ctx ) {
+Shape.prototype.renderCanvasDot = function( ctx , renderer) {
var lineWidth = this.getLineWidth();
if ( !lineWidth ) {
return;
}
- ctx.fillStyle = this.getRenderColor();
var point = this.pathCommands[0].endRenderPoint;
ctx.beginPath();
var radius = lineWidth/2;
ctx.arc( point.x, point.y, radius, 0, TAU );
- ctx.fill();
+ renderer.fill(ctx, null, true, this.getRenderColor() );
};
Shape.prototype.getLineWidth = function() {
@@ -178,7 +190,7 @@ Shape.prototype.getLineWidth = function() {
Shape.prototype.getRenderColor = function() {
// use backface color if applicable
- var isBackfaceColor = typeof this.backface == 'string' && this.isFacingBack;
+ var isBackfaceColor = utils.isColor(this.backface) && this.isFacingBack;
var color = isBackfaceColor ? this.backface : this.color;
return color;
};
diff --git a/js/svg-renderer.js b/js/svg-renderer.js
index 8a2126b..6af5f8d 100644
--- a/js/svg-renderer.js
+++ b/js/svg-renderer.js
@@ -62,12 +62,18 @@ SvgRenderer.stroke = function( svg, elem, isStroke, color, lineWidth ) {
if ( !isStroke ) {
return;
}
+ if (color && color.getSvgFill) {
+ color = color.getSvgFill(svg);
+ }
elem.setAttribute( 'stroke', color );
elem.setAttribute( 'stroke-width', lineWidth );
};
SvgRenderer.fill = function( svg, elem, isFill, color ) {
var fillColor = isFill ? color : 'none';
+ if (fillColor && fillColor.getSvgFill) {
+ fillColor = fillColor.getSvgFill(svg);
+ }
elem.setAttribute( 'fill', fillColor );
};
diff --git a/js/texture.js b/js/texture.js
new file mode 100644
index 0000000..9da0c46
--- /dev/null
+++ b/js/texture.js
@@ -0,0 +1,222 @@
+/**
+ * Texture
+ */
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory(require('./vector'));
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Texture = factory(Zdog.Vector);
+ }
+ }( this, function factory(Vector) {
+
+ /**
+ * Calculates the inverse of the matrix:
+ * | x1 x2 x3 |
+ * | y1 y2 y3 |
+ * | 1 1 1 |
+ */
+ function inverse(x1, y1, x2, y2, x3, y3) {
+ let tp = [
+ y2 - y3, x3 - x2, x2*y3 - x3*y2,
+ y3 - y1, x1 - x3, x3*y1 - x1*y3,
+ y1 - y2, x2 - x1, x1*y2 - x2*y1];
+ let det = tp[2] + tp[5] + tp[8];
+ return tp.map(function(x) { return x / det;});
+ }
+
+ function parsePointMap(size, map) {
+ if (!Array.isArray(map) || !map.length) {
+ map = [0, 0, size[0], size[1]];
+ }
+ if (typeof(map[0]) == "number") {
+ if (map.length < 4) {
+ let tmp = map;
+ map = [0, 0, size[0], size[1]];
+ for (let i = 0; i < tmp.length; i++) {
+ map[i] = tmp[i];
+ }
+ }
+ return [
+ new Vector({x:map[0], y:map[1], z:1}),
+ new Vector({x:map[0] + map[2], y:map[1], z:1}),
+ new Vector({x:map[0], y:map[1] + map[3], z:1})
+ ];
+ } else {
+ return [new Vector(map[0]), new Vector(map[1]), new Vector(map[2])];
+ }
+ }
+
+ var idCounter = 0;
+
+ const optionKeys = [
+ 'img',
+ 'linearGrad',
+ 'radialGrad',
+ 'colorStops',
+ 'src',
+ 'dst'
+ ]
+
+ /**
+ * Creates a tecture map. Possible options:
+ * img: Image object to be used as texture
+ * linearGrad: [x1, y1, x2, y2] Array defining the linear gradient
+ * radialGrad: [x0, y0, r0, x1, y1, r1] Array defining the radial gradient
+ * colorStops: [offset1, color1, offset2, color2...] Array defining the color
+ * stops for the gradient, offset must be in range [0, 1]
+ *
+ * src: Represents the surface for the texture. Above
+ * gradient definition should be represented in this coordinate space
+ * dst: Represents the surface of the object. This allows
+ * keeping the texture definition independent of the surface definition
+ *
+ * Can be represented in one of the following ways:
+ * [x, y, width, height] => We use 3 points top-left, top-right, bottom-left
+ * [x, y] => image/gradient size is used for width and height with the above rule
+ * [vector, vector, vector] => provided points are used
+ */
+ function Texture(options) {
+ this.id = idCounter++;
+ this.isTexture = true;
+
+ options = options || { }
+ for (var key in options ) {
+ if (optionKeys.indexOf( key ) != -1 ) {
+ this[key] = options[key];
+ }
+ }
+
+ var size;
+ if (options.img) {
+ size = [options.img.width, options.img.height];
+ } else if (options.linearGrad) {
+ size = [Math.abs(options.linearGrad[2] - options.linearGrad[0]), Math.abs(options.linearGrad[3] - options.linearGrad[1])];
+ } else if (options.radialGrad) {
+ size = [Math.abs(options.radialGrad[3] - options.radialGrad[0]), Math.abs(options.radialGrad[4] - options.radialGrad[1])];
+ } else {
+ throw "One of [img, linearGrad, radialGrad] is required";
+ }
+ if (size[0] == 0) size[0] = size[1];
+ if (size[1] == 0) size[1] = size[0];
+
+ this.src = parsePointMap(size, options.src);
+ this.dst = parsePointMap(size, options.dst);
+
+ this.srcInverse = inverse(
+ this.src[0].x, this.src[0].y,
+ this.src[1].x, this.src[1].y,
+ this.src[2].x, this.src[2].y);
+ this.p1 = new Vector();
+ this.p2 = new Vector();
+ this.p3 = new Vector();
+ this.matrix = [0, 0, 0, 0, 0, 0];
+ };
+
+ Texture.prototype.getMatrix = function() {
+ let m = this.matrix;
+ let inverse = this.srcInverse;
+ m[0] = this.p1.x * inverse[0] + this.p2.x * inverse[3] + this.p3.x * inverse[6];
+ m[1] = this.p1.y * inverse[0] + this.p2.y * inverse[3] + this.p3.y * inverse[6];
+ m[2] = this.p1.x * inverse[1] + this.p2.x * inverse[4] + this.p3.x * inverse[7];
+ m[3] = this.p1.y * inverse[1] + this.p2.y * inverse[4] + this.p3.y * inverse[7];
+ m[4] = this.p1.x * inverse[2] + this.p2.x * inverse[5] + this.p3.x * inverse[8];
+ m[5] = this.p1.y * inverse[2] + this.p2.y * inverse[5] + this.p3.y * inverse[8];
+ return m;
+ }
+
+ Texture.prototype.getCanvasFill = function(ctx) {
+ if (!this.pattern) {
+ if (this.img) {
+ this.pattern = ctx.createPattern(this.img, "repeat");
+ } else {
+ this.pattern = this.linearGrad
+ ? ctx.createLinearGradient.apply(ctx, this.linearGrad)
+ : ctx.createRadialGradient.apply(ctx, this.radialGrad);
+ if (this.colorStops) {
+ for (var i = 0; i < this.colorStops.length; i+=2) {
+ this.pattern.addColorStop(this.colorStops[i], this.colorStops[i+1]);
+ }
+ }
+ }
+ }
+ // pattern.setTransform is not supported in IE,
+ // so transform the context instead
+ ctx.transform.apply(ctx, this.getMatrix());
+ return this.pattern;
+ };
+
+ const svgURI = 'http://www.w3.org/2000/svg';
+ Texture.prototype.getSvgFill = function(svg) {
+ if (!this.svgPattern) {
+ if (this.img) {
+ this.svgPattern = document.createElementNS( svgURI, 'pattern');
+ this.svgPattern.setAttribute("width", this.img.width);
+ this.svgPattern.setAttribute("height", this.img.height);
+ this.svgPattern.setAttribute("patternUnits", "userSpaceOnUse");
+ this.attrTransform = "patternTransform";
+
+ let img = document.createElementNS( svgURI, 'image');
+ img.setAttribute("href", this.img.src);
+ this.svgPattern.appendChild(img);
+ } else {
+ var type, vals, keys;
+ if (this.linearGrad) {
+ type = "linearGradient";
+ vals = this.linearGrad;
+ keys = ["x1", "y1", "x2", "y2"]
+ } else {
+ type = "radialGradient";
+ vals = this.radialGrad;
+ keys = ["fx", "fy", "fr", "cx", "cy", "r"]
+ }
+ this.svgPattern = document.createElementNS( svgURI, type);
+ for (var i = 0; i < keys.length; i++) {
+ this.svgPattern.setAttribute(keys[i], vals[i]);
+ }
+
+ if (this.colorStops) {
+ for (var i = 0; i < this.colorStops.length; i+=2) {
+ let colorStop = document.createElementNS(svgURI, 'stop' );
+ colorStop.setAttribute("offset", this.colorStops[i]);
+ colorStop.setAttribute("style", "stop-color:" + this.colorStops[i+1]);
+ this.svgPattern.appendChild(colorStop);
+ }
+ }
+ this.svgPattern.setAttribute("gradientUnits", "userSpaceOnUse");
+ this.attrTransform = "gradientTransform";
+ }
+ this.svgPattern.setAttribute("id", "texture_" + this.id);
+ this._svgUrl = 'url(#texture_' + this.id + ')';
+
+ this.defs = document.createElementNS(svgURI, 'defs' );
+ this.defs.appendChild(this.svgPattern);
+ }
+
+ this.svgPattern.setAttribute(this.attrTransform, 'matrix(' + this.getMatrix().join(' ') + ')');
+ svg.appendChild( this.defs );
+ return this._svgUrl;
+ }
+
+ // ----- update ----- //
+ Texture.prototype.reset = function() {
+ this.p1.set(this.dst[0]);
+ this.p2.set(this.dst[1]);
+ this.p3.set(this.dst[2]);
+ };
+
+ Texture.prototype.transform = function( translation, rotation, scale ) {
+ this.p1.transform(translation, rotation, scale);
+ this.p2.transform(translation, rotation, scale);
+ this.p3.transform(translation, rotation, scale);
+ };
+
+ Texture.prototype.clone = function() {
+ return new Texture(this);
+ };
+
+ return Texture;
+} ) );