GLSL Rounded Rectangle with Variable Border - geometry

I'm trying to write a GLSL shader that produces a rounded rectangle with a border, similar to the example below but where each border edge (top, bottom, left, right) can have a different thickness.
So, rather than a uniform borderThickness, we would have:
borderThicknessX0, borderThicknessX1, borderThicknessY0, borderThicknessY1
I've looked at numerous examples of creating a rounded rect in a shader using signed-distance fields (SDF), but have not figured out how to adapt the code to allow for variable border edge thickness.
It seems many approaches involve abs() of the fragment coordinate, therefore assuming uniformity.
Can anyone provide any guidance on how to achieve this?
Thanks!
example: rounded rect with constant border
example: rounded rect with no border

In compare to the shadertoy shader (rounded rect with constant border) you've to calculate the u_fHalfBorderThickness dependent on the fragment.
Define a thickness for the left, right, bottom and top:
float u_ThicknessTop = 20.0;
float u_ThicknessBottom = 30.0;
float u_ThicknessLeft = 25.0;
float u_ThicknessRight = 35.0;
Calculate the thickness of the edges dependent on the section:
vec2 uv = fragCoord / iResolution;
vec2 edgeThickness = vec2(
uv.x > 0.5 ? u_ThicknessRight : u_ThicknessLeft,
uv.y > 0.5 ? u_ThicknessTop : u_ThicknessBottom );
Calculate the fHalfBorderThicknessdependent on the fragment and the distance (fHalfBorderDist) of the fragment to the center of the border:
float fHalfBorderDist = 0.0;
float fHalfBorderThickness = 0.0;
if (fragCoord.x > max(u_fRadiusPx, u_ThicknessLeft) &&
fragCoord.x < u_resolution.x - max(u_fRadiusPx, u_ThicknessRight))
{
fHalfBorderDist = v2CenteredPos.y - v2HalfShapeSizePx.y;
fHalfBorderThickness = v2edgeThickness.y / 2.0;
}
else if (fragCoord.y > max(u_fRadiusPx, u_ThicknessBottom) &&
fragCoord.y < u_resolution.y - max(u_fRadiusPx, u_ThicknessTop))
{
fHalfBorderDist = v2CenteredPos.x - v2HalfShapeSizePx.x;
fHalfBorderThickness = v2edgeThickness.x / 2.0;
}
else
{
vec2 edgeVec = max(vec2(0.0), u_fRadiusPx - vec2(
uv.x > 0.5 ? iResolution.x-fragCoord.x : fragCoord.x,
uv.y > 0.5 ? iResolution.y-fragCoord.y : fragCoord.y));
vec2 ellipse_ab = u_fRadiusPx-v2edgeThickness;
vec2 ellipse_isect = (v2edgeThickness.x > u_fRadiusPx || v2edgeThickness.y > u_fRadiusPx) ? vec2(0.0) :
edgeVec.xy * ellipse_ab.x*ellipse_ab.y / length(ellipse_ab*edgeVec.yx);
fHalfBorderThickness = (u_fRadiusPx - length(ellipse_isect)) / 2.0;
fHalfBorderDist = length(edgeVec) - (u_fRadiusPx - fHalfBorderThickness);
}
Note, the inner rounding of the border is an ellipse. To verify if a point is on the border, you've to intersect the line from the center point of the rounding, to the current fragment with the ellipse. See Ellipse-Line Intersection
vec2 ellipse_ab = u_fRadiusPx-v2edgeThickness;
vec2 ellipse_isect = edgeVec.xy * ellipse_ab.x*ellipse_ab.y / length(ellipse_ab*edgeVec.yx);
See the example:
(function loadscene() {
var canvas, gl, vp_size, prog, bufObj = {};
function initScene() {
canvas = document.getElementById( "ogl-canvas");
gl = canvas.getContext( "experimental-webgl" );
if ( !gl )
return;
progDraw = gl.createProgram();
for (let i = 0; i < 2; ++i) {
let source = document.getElementById(i==0 ? "draw-shader-vs" : "draw-shader-fs").text;
let shaderObj = gl.createShader(i==0 ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER);
gl.shaderSource(shaderObj, source);
gl.compileShader(shaderObj);
let status = gl.getShaderParameter(shaderObj, gl.COMPILE_STATUS);
if (!status) alert(gl.getShaderInfoLog(shaderObj));
gl.attachShader(progDraw, shaderObj);
gl.linkProgram(progDraw);
}
status = gl.getProgramParameter(progDraw, gl.LINK_STATUS);
if ( !status ) alert(gl.getProgramInfoLog(progDraw));
progDraw.inPos = gl.getAttribLocation(progDraw, "inPos");
progDraw.u_resolution = gl.getUniformLocation(progDraw, "u_resolution");
progDraw.u_ThicknessTop = gl.getUniformLocation(progDraw, "u_ThicknessTop");
progDraw.u_ThicknessBottom = gl.getUniformLocation(progDraw, "u_ThicknessBottom");
progDraw.u_ThicknessLeft = gl.getUniformLocation(progDraw, "u_ThicknessLeft");
progDraw.u_ThicknessRight = gl.getUniformLocation(progDraw, "u_ThicknessRight");
gl.useProgram(progDraw);
var pos = [ -1, -1, 1, -1, 1, 1, -1, 1 ];
var inx = [ 0, 1, 2, 0, 2, 3 ];
bufObj.pos = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, bufObj.pos );
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( pos ), gl.STATIC_DRAW );
bufObj.inx = gl.createBuffer();
bufObj.inx.len = inx.length;
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, bufObj.inx );
gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( inx ), gl.STATIC_DRAW );
gl.enableVertexAttribArray( progDraw.inPos );
gl.vertexAttribPointer( progDraw.inPos, 2, gl.FLOAT, false, 0, 0 );
gl.enable( gl.DEPTH_TEST );
gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
window.onresize = resize;
resize();
requestAnimationFrame(render);
}
function resize() {
//vp_size = [gl.drawingBufferWidth, gl.drawingBufferHeight];
vp_size = [window.innerWidth, window.innerHeight];
//vp_size = [256, 256]
canvas.width = vp_size[0];
canvas.height = vp_size[1];
}
function render(deltaMS) {
var top = document.getElementById("top").value;
var bottom = document.getElementById("bottom").value;
var left = document.getElementById("left").value;
var right = document.getElementById("right").value;
gl.viewport( 0, 0, canvas.width, canvas.height );
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
gl.uniform2f(progDraw.u_resolution, canvas.width, canvas.height);
gl.uniform1f(progDraw.u_ThicknessTop, top);
gl.uniform1f(progDraw.u_ThicknessBottom, bottom);
gl.uniform1f(progDraw.u_ThicknessLeft, left);
gl.uniform1f(progDraw.u_ThicknessRight, right);
gl.drawElements( gl.TRIANGLES, bufObj.inx.len, gl.UNSIGNED_SHORT, 0 );
requestAnimationFrame(render);
}
initScene();
})();
#gui { position : absolute; top : 0; left : 0; font-size : large; }
<script id="draw-shader-vs" type="x-shader/x-vertex">
precision mediump float;
attribute vec2 inPos;
void main()
{
//ndcPos = inPos;
gl_Position = vec4( inPos.xy, 0.0, 1.0 );
}
</script>
<script id="draw-shader-fs" type="x-shader/x-fragment">
precision mediump float;
//varying vec2 ndcPos; // normaliced device coordinates in range [-1.0, 1.0]
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_ThicknessTop;
uniform float u_ThicknessBottom;
uniform float u_ThicknessLeft;
uniform float u_ThicknessRight;
const vec4 u_v4BorderColor = vec4(1.0, 1.0, 0.0, 1.0);
const vec4 u_v4FillColor = vec4(1.0, 0.0, 0.0, 1.0);
const float u_fRadiusPx = 50.0;
void main()
{
vec2 iResolution = u_resolution;
vec2 fragCoord = gl_FragCoord.xy;
vec2 uv = fragCoord / iResolution;
vec2 v2edgeThickness = vec2(
uv.x > 0.5 ? u_ThicknessRight : u_ThicknessLeft,
uv.y > 0.5 ? u_ThicknessTop : u_ThicknessBottom );
vec2 v2CenteredPos = abs(fragCoord - iResolution.xy / 2.0);
vec2 v2HalfShapeSizePx = iResolution/2.0 - v2edgeThickness/2.0;
float fHalfBorderDist = 0.0;
float fHalfBorderThickness = 0.0;
if (fragCoord.x > max(u_fRadiusPx, u_ThicknessLeft) &&
fragCoord.x < u_resolution.x - max(u_fRadiusPx, u_ThicknessRight))
{
fHalfBorderDist = v2CenteredPos.y - v2HalfShapeSizePx.y;
fHalfBorderThickness = v2edgeThickness.y / 2.0;
}
else if (fragCoord.y > max(u_fRadiusPx, u_ThicknessBottom) &&
fragCoord.y < u_resolution.y - max(u_fRadiusPx, u_ThicknessTop))
{
fHalfBorderDist = v2CenteredPos.x - v2HalfShapeSizePx.x;
fHalfBorderThickness = v2edgeThickness.x / 2.0;
}
else
{
vec2 edgeVec = max(vec2(0.0), u_fRadiusPx - vec2(
uv.x > 0.5 ? iResolution.x-fragCoord.x : fragCoord.x,
uv.y > 0.5 ? iResolution.y-fragCoord.y : fragCoord.y));
vec2 ellipse_ab = u_fRadiusPx-v2edgeThickness;
vec2 ellipse_isect = (v2edgeThickness.x > u_fRadiusPx || v2edgeThickness.y > u_fRadiusPx) ? vec2(0.0) :
edgeVec.xy * ellipse_ab.x*ellipse_ab.y / length(ellipse_ab*edgeVec.yx);
fHalfBorderThickness = (u_fRadiusPx - length(ellipse_isect)) / 2.0;
fHalfBorderDist = length(edgeVec) - (u_fRadiusPx - fHalfBorderThickness);
}
vec4 v4FromColor = u_v4BorderColor;
vec4 v4ToColor = vec4(0.0, 0.0, 1.0, 1.0);
if (fHalfBorderDist < 0.0)
v4ToColor = u_v4FillColor;
gl_FragColor = mix(v4FromColor, v4ToColor, abs(fHalfBorderDist) - fHalfBorderThickness);
}
</script>
<div><form id="gui" name="inputs">
<table>
<tr> <td> top </td>
<td> <input type="range" id="top" min="1" max="100" value="20"/>
</td> </tr>
<tr> <td> bottom </td>
<td> <input type="range" id="bottom" min="1" max="100" value="30"/>
</td> </tr>
<tr> <td> left </td>
<td> <input type="range" id="left" min="1" max="100" value="25" />
</td></tr>
<tr> <td> right </td>
<td> <input type="range" id="right" min="1" max="100" value="35"/>
</td> </tr>
</table>
</form></div>
<canvas id="ogl-canvas" style="border: none"></canvas>

Related

Three.js audio reactive shader page single page PWA

Why is this not canvas not drawing?
Tried adjusting three.js grammar, but no results yet.
I'm a bit lost as to what might need to be different, although have highlighted some dodgy looking punctution functions. I'm used ot GLSL rather than javascript.
This is not working correctly below
var shaderMaterial = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: `
void main() {
gl_Position = vec4( position, 1.0 );
}
`,
fragmentShader:
uniform vec2 iResolution;
uniform float iGlobalTime;
uniform float iChannel0[32];
uniform float speed;
vec2 rotate(vec2 v, float a) {
return vec2(cos(a)*v.x-sin(a)*v.y, sin(a)*v.x+cos(a)*v.y);
}
void main() {
vec2 uv = gl_FragCoord.xy / iResolution.xy - 0.5;
uv = rotate(uv, iGlobalTime * 0.1);
uv *= 1.0 + 0.5 * iChannel0[0] * speed;
float v = sin(uv.x * 10.0) * 0.5 + 0.5;
v = pow(v, 2.0);
gl_FragColor = vec4( vec3( v ), 1.0 );
}
});
// Render loop
function render() {
requestAnimationFrame(render);
uniforms.iGlobalTime.value += 0.05;
uniforms.speed.value = 1 + 0.5 * analyser.getAverageFrequency() / 256;
renderer.render(scene, camera);
}
render();
})
.catch(function (error) {
console.error(error);
});
</script>
</body>
</html>

How to properly blend colors across two triangles and remove diagonal smear

I am learning WebGL and I've drawn a full screen quad with colors for each vertex. No lighting or normals or perspective matrix or depth buffer; I'm just drawing a gradient background. This is what I get:
It looks good but I cannot help noticing the diagonal smear from the bottom right to the top left. I feel this is an artifact of linear interpolating the far opposite vertices. I'm drawing two triangles: the bottom left, and the top right. I think I would get similar results using OpenGL instead of WebGL.
Given the same four colors and the same size rectangle, is there a way to render this so the edge between the two triangles isn't so apparent? Maybe more vertices, or a different blending function? I'm not sure exactly what the colors should be at each pixel; I just want to know how to get rid of the diagonal smear.
The issue is the top right triangle has no knowledge of the bottom left corner so the top right triangle is not including any of the blue from the bottom left (and visa versa)
A couple of ways to fix that.
One is to use a 2x2 texture with linear sampling. You have to do some extra math to get the interpolation correct because a texture only interpolates between pixels
+-------+-------+
| | |
| +-------+ |
| | | | |
+---|---+---|---+
| | | | |
| +-------+ |
| | |
+-------+-------+
Above is a 4 pixel texture stretched to 14 by 6. Sampling happens between pixels so only this center area will get the gradient. Outside that area would be sampled with pixels outside the texture so using CLAMP_TO_EDGE or on the opposite side of the texture using REPEAT.
const gl = document.querySelector('canvas').getContext('webgl');
const tl = [254, 217, 138];
const tr = [252, 252, 252];
const bl = [18, 139, 184];
const br = [203, 79, 121];
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.texImage2D(
gl.TEXTURE_2D,
0, // mip level
gl.RGB, // internal format
2, // width,
2, // height,
0, // border
gl.RGB, // format
gl.UNSIGNED_BYTE, // type
new Uint8Array([...bl, ...br, ...tl, ...tr]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = texcoord;
}
`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
const vec2 texSize = vec2(2, 2); // could pass this in
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex,
(v_texcoord * (texSize - 1.0) + 0.5) / texSize);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const texcoordLoc = gl.getAttribLocation(program, 'texcoord');
function createBufferAndSetupAttribute(loc, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(
loc,
2, // 2 elements per iteration
gl.FLOAT, // type of data in buffer
false, // normalize
0, // stride
0, // offset
);
}
createBufferAndSetupAttribute(positionLoc, [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]);
createBufferAndSetupAttribute(texcoordLoc, [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
]);
gl.useProgram(program);
// note: no need to set sampler uniform as it defaults
// to 0 which is what we'd set it to anyway.
gl.drawArrays(gl.TRIANGLES, 0, 6);
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
Note: to see what I mean about the extra math needed for the texture coordinates here is the same example without the extra math
const gl = document.querySelector('canvas').getContext('webgl');
const tl = [254, 217, 138];
const tr = [252, 252, 252];
const bl = [18, 139, 184];
const br = [203, 79, 121];
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.texImage2D(
gl.TEXTURE_2D,
0, // mip level
gl.RGB, // internal format
2, // width,
2, // height,
0, // border
gl.RGB, // format
gl.UNSIGNED_BYTE, // type
new Uint8Array([...bl, ...br, ...tl, ...tr]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = texcoord;
}
`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const texcoordLoc = gl.getAttribLocation(program, 'texcoord');
function createBufferAndSetupAttribute(loc, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(
loc,
2, // 2 elements per iteration
gl.FLOAT, // type of data in buffer
false, // normalize
0, // stride
0, // offset
);
}
createBufferAndSetupAttribute(positionLoc, [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]);
createBufferAndSetupAttribute(texcoordLoc, [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
]);
gl.useProgram(program);
// note: no need to set sampler uniform as it defaults
// to 0 which is what we'd set it to anyway.
gl.drawArrays(gl.TRIANGLES, 0, 6);
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
Also of course, rather than do the math in the fragment shader we could fix the texture coordinates in JavaScript
const gl = document.querySelector('canvas').getContext('webgl');
const tl = [254, 217, 138];
const tr = [252, 252, 252];
const bl = [18, 139, 184];
const br = [203, 79, 121];
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.texImage2D(
gl.TEXTURE_2D,
0, // mip level
gl.RGB, // internal format
2, // width,
2, // height,
0, // border
gl.RGB, // format
gl.UNSIGNED_BYTE, // type
new Uint8Array([...bl, ...br, ...tl, ...tr]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = texcoord;
}
`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const texcoordLoc = gl.getAttribLocation(program, 'texcoord');
function createBufferAndSetupAttribute(loc, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(
loc,
2, // 2 elements per iteration
gl.FLOAT, // type of data in buffer
false, // normalize
0, // stride
0, // offset
);
}
createBufferAndSetupAttribute(positionLoc, [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]);
createBufferAndSetupAttribute(texcoordLoc, [
0.25, 0.25,
0.75, 0.25,
0.25, 0.75,
0.25, 0.75,
0.75, 0.25,
0.75, 0.75,
]);
gl.useProgram(program);
// note: no need to set sampler uniform as it defaults
// to 0 which is what we'd set it to anyway.
gl.drawArrays(gl.TRIANGLES, 0, 6);
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
Another way is to do the interpolation yourself based on those corners (which is effectively doing what the texture sampler is doing in the previous example, bi-linear interpolation of the 4 colors).
const gl = document.querySelector('canvas').getContext('webgl');
const tl = [254/255, 217/255, 138/255];
const tr = [252/255, 252/255, 252/255];
const bl = [ 18/255, 139/255, 184/255];
const br = [203/255, 79/255, 121/255];
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = texcoord;
}
`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform vec3 tl;
uniform vec3 tr;
uniform vec3 bl;
uniform vec3 br;
void main() {
vec3 l = mix(bl, tl, v_texcoord.t);
vec3 r = mix(br, tr, v_texcoord.t);
vec3 c = mix(l, r, v_texcoord.s);
gl_FragColor = vec4(c, 1);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const texcoordLoc = gl.getAttribLocation(program, 'texcoord');
const tlLoc = gl.getUniformLocation(program, 'tl');
const trLoc = gl.getUniformLocation(program, 'tr');
const blLoc = gl.getUniformLocation(program, 'bl');
const brLoc = gl.getUniformLocation(program, 'br');
function createBufferAndSetupAttribute(loc, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(
loc,
2, // 2 elements per iteration
gl.FLOAT, // type of data in buffer
false, // normalize
0, // stride
0, // offset
);
}
createBufferAndSetupAttribute(positionLoc, [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]);
createBufferAndSetupAttribute(texcoordLoc, [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
]);
gl.useProgram(program);
gl.uniform3fv(tlLoc, tl);
gl.uniform3fv(trLoc, tr);
gl.uniform3fv(blLoc, bl);
gl.uniform3fv(brLoc, br);
gl.drawArrays(gl.TRIANGLES, 0, 6);
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
Number of dimensions
You should be passing the 2D coordinate space of the quad to the fragment shader rather than the one dimensional (per channel) color space.
Then in the fragment shader you can do the color interpolation in 2D space removing the color artifact due to the diagonal line interpolating in 1D.
The shader snippet for linear color interpolation, where coord2D is the 2D coordinate space
pixel = vec4(vec3(mix(
mix(colors[0], colors[1], coord2D.x),
mix(colors[2], colors[3], coord2D.x),
coord2D.y
)), 1);
Improved color interpolation
When interpolating colors by their RGB values the results can visually darken between opposing hues.
An easy fix is to use a closer approximation of the sRGB color model by interpolating between the squares of the color channel values. The final output is the square root of the interpolated values.
The interpolation snippet.
pixel = vec4(sqrt(vec3(mix(
mix(colors[0], colors[1], coord2D.x),
mix(colors[2], colors[3], coord2D.x),
coord2D.y
))) / 255.0, 1);
Note that the color channel values in the uniform colors are in logarithmic space. [R^2, G^2, B^2] and thus range from 0 to 65025.
Example
In the example click the canvas to switch between interpolation methods.
You will note that when using approx ~sRGB that the brightness in the center of the canvas out towards the center edges does not dip as much below the perceivable brightness at the corners.
Also note that the balance of the transition from the bottom blue and redish to the top orange and white moves down closer the the center. This is because interpolating the RGB model will darken colors that have strong components from 2 or more channels, Reds, greens, blues and blacks will dominate over yellows, cyans, magentas and whites making the interpolation seem to shift and stretch out the RGB primaries.
var program, colorsLoc, modelLoc, loc, text = " interpolation. Click for ", model = "RGB"; // or sRGB
const vertSrc = `#version 300 es
in vec2 verts;
out vec2 coord2D;
void main() {
coord2D = verts * 0.5 + 0.5; // convert to quad space 0,0 <=> 1, 1
gl_Position = vec4(verts, 1, 1);
}`;
const fragSrc = `#version 300 es
#define channelMax 255.0
// color location indexes
#define TR 3
#define TL 2
#define BR 1
#define BL 0
precision mediump float;
uniform vec3 colors[4];
uniform bool isRGB;
in vec2 coord2D;
out vec4 pixel;
void main() {
if (isRGB) {
pixel = vec4(vec3(mix(
mix(colors[BL], colors[BR], coord2D.x),
mix(colors[TL], colors[TR], coord2D.x),
coord2D.y
)) / channelMax, 1);
} else {
pixel = vec4(vec3(sqrt(mix(
mix(colors[BL], colors[BR], coord2D.x),
mix(colors[TL], colors[TR], coord2D.x),
coord2D.y
))) / channelMax, 1);
}
}`;
const fArr = arr => new Float32Array(arr);
const colors = [64,140,190, 224,81,141, 247,223,140, 245,245,245];
const gl = canvas.getContext("webgl2", {premultipliedAlpha: false, antialias: false, alpha: false});
addEventListener("resize", draw);
addEventListener("click", draw);
setup();
draw();
function compileShader(src, type, shader = gl.createShader(type)) {
gl.shaderSource(shader, src);
gl.compileShader(shader);
return shader;
}
function setup() {
program = gl.createProgram();
gl.attachShader(program, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(program, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(program);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([0,1,2,0,2,3]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, fArr([-1,-1,1,-1,1,1,-1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc = gl.getAttribLocation(program, "verts"));
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
colorsLoc = gl.getUniformLocation(program, "colors");
modelLoc = gl.getUniformLocation(program, "isRGB");
gl.useProgram(program);
}
function draw() {
[info.textContent, model] = model != "RGB"? [`RGB${text}~sRGB.`, "RGB"]: [`~sRGB${text}RGB.`, "~sRGB"];
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
[canvas.width, canvas.height] = [innerWidth, innerHeight];
gl.viewport(0, 0, canvas.width, canvas.height);
}
gl.uniform3fv(colorsLoc, fArr(colors.map(v => model=="RGB"? v: v*v)), 0, 12);
gl.uniform1i(modelLoc, model=="RGB");
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
body {
padding: 0px;
margin: 0px;
font-family: arial;
color: white;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
h2 {
position: absolute;
bottom: 0px;
left: 0px;
right: 0px;
text-align: center;
}
<canvas id="canvas"></canvas>
<h2 id="info"></h2>

Animating lots of lines in WebGL

I'm trying to animate this scene at 1280x720, 60 fps, antialiased. There are ~500K triangles, two for each tree branch. I can't use gl.LINES because ANGLE.
To get a rough idea about the performance I can expect, I made this test with 1M of random triangles:
<!doctype html>
<head>
<title>Triangles</title>
<style>
html, body {
background: #000;
height: 100%;
margin: 0;
}
canvas {
width: 1280px;
height: 720px;
position: absolute;
margin: auto;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
</style>
</head>
<body>
<script>
'use strict';
const triangleCount = 1e6;
const antialias = true;
const generateTriangles = (count, width, height) => {
const coords = new Float32Array(9 * count);
for (var i = 0; i < coords.length;) {
const x = Math.random() * 2 - 1;
const y = Math.random() * 2 - 1;
const z = Math.random() * 2 - 1;
const theta = Math.random() * Math.PI;
const ax = 10 * Math.cos(theta) / width;
const ay = 10 * Math.sin(theta) / height;
const bx = 10 * Math.cos(theta + 0.3) / width;
const by = 10 * Math.sin(theta + 0.3) / height;
coords[i++] = x + ax; coords[i++] = y + ay; coords[i++] = z;
coords[i++] = x + bx; coords[i++] = y + by; coords[i++] = z;
coords[i++] = x - ax; coords[i++] = y - ay; coords[i++] = z;
coords[i++] = x - ax; coords[i++] = y - ay; coords[i++] = z;
coords[i++] = x - bx; coords[i++] = y - by; coords[i++] = z;
coords[i++] = x + ax; coords[i++] = y + ay; coords[i++] = z;
}
return coords;
};
const vertexShaderSource = `
precision lowp float;
attribute vec3 aPosition;
uniform float uWobble;
void main() {
float p = 1.0 / (0.3 * aPosition.z - 1.4 + 0.1 * uWobble);
gl_Position = vec4(p * aPosition.x, p * aPosition.y, aPosition.z, 1.0);
}
`;
const fragmentShaderSource = `
precision lowp float;
void main() {
float z = gl_FragCoord.z;
gl_FragColor = vec4(1.2 * z, z * z, z, 1.0);
}
`;
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const gl = canvas.getContext('webgl', {alpha: false, antialias});
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
const aVertexPosition = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(aVertexPosition);
const uWobble = gl.getUniformLocation(program, 'uWobble');
gl.uniform1f(uWobble, 1);
const vertices = generateTriangles(triangleCount, canvas.width, canvas.height);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0);
const render = (timestamp) => {
requestAnimationFrame(render);
gl.uniform1f(uWobble, Math.sin(0.002 * timestamp));
gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 3);
};
window.requestAnimationFrame(render);
</script>
</body>
On a Macbook with Iris 1536 MB I'm getting 12 frames per second. Are there any optimizations I'm missing, such as alternative ways to draw lines, faster antialiasing, some magic flags? Or is this basically the limit of what one can expect from an average GPU?

Multiple WebGL models on the same page

My research lab is working on a webpage that displays a long scrollable list of 3d models, about 50 or so. Our first idea was to do this with separate THREE.js WebGL contexts, but it seems this isn't advisable given the architecture of WebGL, and browsers seem to limit the number of contexts on a page to about 2^4.
I don't need these contexts to do anything very impressive: the individual geometries only have a few hundred triangles, with no textures, and only one at a time ever animates when using the mouse to rotate its camera.
Can I persuade WebGL to do what I want in a way that the browser won't complain about? I thought perhaps of having a single big geometry with all my individual models lined up next to each other, and separate canvases with viewports showing just one model each. But it seems that isn't supported. (Multiple views are allowed in the same context, but that's not very useful for me.)
Thanks for any ideas!
It's not clear why you think you need multiple webgl contexts. I'm guessing because you want a list like this
1. [img] description
description
2. [img] description
description
3. [img] description
description
Or something?
Some ideas
make one canvas big enough for the screen, set its CSS so it doesn't scroll with the rest of the page. Draw the models aligned with whatever other HTML you want that does scroll.
make an offscreen webgl canvas and use canvas2d elements to display.
For each model render the model and then call
someCanvas2DContextForElementN.drawImage(webGLcanvasElement, ...);
Given there are probably only ever a few canvases visible you only need to update those ones. In fact it's probably a good idea to recycle them. In other words, rather than make 12000 canvaes or a 12000 element list make just enough to fit on the screen and update them as you scroll.
Personally I'd probably pick #1 if my page design allowed it. Seems to work, see below.
It turned out to be really easy. I just took this sample that was drawing 100 objects and made it draw one object at a time.
After clearing the screen turn on the scissor test
gl.enable(gl.SCISSOR_TEST);
Then, for each object
// get the element that is a place holder for where we want to
// draw the object
var viewElement = obj.viewElement;
// get its position relative to the page's viewport
var rect = viewElement.getBoundingClientRect();
// check if it's offscreen. If so skip it
if (rect.bottom < 0 || rect.top > gl.canvas.clientHeight ||
rect.right < 0 || rect.left > gl.canvas.clientWidth) {
return; // it's off screen
}
// set the viewport
var width = rect.right - rect.left;
var height = rect.bottom - rect.top;
var left = rect.left;
var bottom = gl.canvas.clientHeight - rect.bottom - 1;
gl.viewport(left, bottom, width, height);
gl.scissor(left, bottom, width, height);
I'm not 100% sure if I need to add 1 the width and height or not. I suppose I should look that up.
In any case I compute a new projection matrix for every rendered object just to make the code generic. The placeholder divs could be different sizes.
Update:
the solution originally posted here used position: fixed on the canvas to keep it from scrolling. The new solution uses position: absolute and updates the transform just before rendering like this
gl.canvas.style.transform = `translateY(${window.scrollY}px)`;
With the previous solution the shapes getting re-drawn in their matching positions could lag behind the scrolling. With the new solution the canvas scrolls until we get time to update it. That means shapes might be missing for a few frames if we can't draw quick enough but it looks much better than the scrolling not matching.
The sample below is the updated solution.
"use strict";
// using twgl.js because I'm lazy
twgl.setAttributePrefix("a_");
var m4 = twgl.m4;
var gl = twgl.getWebGLContext(document.getElementById("c"));
// compiles shaders, links program, looks up locations
var programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
// calls gl.creatBuffer, gl.bindBuffer, gl.bufferData for each shape
// for positions, normals, texcoords
var shapes = [
twgl.primitives.createCubeBufferInfo(gl, 2),
twgl.primitives.createSphereBufferInfo(gl, 1, 24, 12),
twgl.primitives.createPlaneBufferInfo(gl, 2, 2),
twgl.primitives.createTruncatedConeBufferInfo(gl, 1, 0, 2, 24, 1),
twgl.primitives.createCresentBufferInfo(gl, 1, 1, 0.5, 0.1, 24),
twgl.primitives.createCylinderBufferInfo(gl, 1, 2, 24, 2),
twgl.primitives.createDiscBufferInfo(gl, 1, 24),
twgl.primitives.createTorusBufferInfo(gl, 1, 0.4, 24, 12),
];
function rand(min, max) {
return min + Math.random() * (max - min);
}
// Shared values
var lightWorldPosition = [1, 8, -10];
var lightColor = [1, 1, 1, 1];
var camera = m4.identity();
var view = m4.identity();
var viewProjection = m4.identity();
var tex = twgl.createTexture(gl, {
min: gl.NEAREST,
mag: gl.NEAREST,
src: [
255, 255, 255, 255,
192, 192, 192, 255,
192, 192, 192, 255,
255, 255, 255, 255,
],
});
var randColor = function() {
var color = [Math.random(), Math.random(), Math.random(), 1];
color[Math.random() * 3 | 0] = 1; // make at least 1 bright
return color;
};
var objects = [];
var numObjects = 100;
var list = document.getElementById("list");
var listItemTemplate = document.getElementById("list-item-template").text;
for (var ii = 0; ii < numObjects; ++ii) {
var listElement = document.createElement("div");
listElement.innerHTML = listItemTemplate;
listElement.className = "list-item";
var viewElement = listElement.querySelector(".view");
var uniforms = {
u_lightWorldPos: lightWorldPosition,
u_lightColor: lightColor,
u_diffuseMult: randColor(),
u_specular: [1, 1, 1, 1],
u_shininess: 50,
u_specularFactor: 1,
u_diffuse: tex,
u_viewInverse: camera,
u_world: m4.identity(),
u_worldInverseTranspose: m4.identity(),
u_worldViewProjection: m4.identity(),
};
objects.push({
ySpeed: rand(0.1, 0.3),
zSpeed: rand(0.1, 0.3),
uniforms: uniforms,
viewElement: viewElement,
programInfo: programInfo,
bufferInfo: shapes[ii % shapes.length],
});
list.appendChild(listElement);
}
var showRenderingArea = false;
function render(time) {
time *= 0.001;
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.canvas.style.transform = `translateY(${window.scrollY}px)`;
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.SCISSOR_TEST);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.SCISSOR_TEST);
if (showRenderingArea) {
gl.clearColor(0, 0, 1, 1);
}
var eye = [0, 0, -8];
var target = [0, 0, 0];
var up = [0, 1, 0];
m4.lookAt(eye, target, up, camera);
m4.inverse(camera, view);
objects.forEach(function(obj, ndx) {
var viewElement = obj.viewElement;
// get viewElement's position
var rect = viewElement.getBoundingClientRect();
if (rect.bottom < 0 || rect.top > gl.canvas.clientHeight ||
rect.right < 0 || rect.left > gl.canvas.clientWidth) {
return; // it's off screen
}
var width = rect.right - rect.left;
var height = rect.bottom - rect.top;
var left = rect.left;
var bottom = gl.canvas.clientHeight - rect.bottom - 1;
gl.viewport(left, bottom, width, height);
gl.scissor(left, bottom, width, height);
if (showRenderingArea) {
gl.clear(gl.COLOR_BUFFER_BIT);
}
var projection = m4.perspective(30 * Math.PI / 180, width / height, 0.5, 100);
m4.multiply(projection, view, viewProjection);
var uni = obj.uniforms;
var world = uni.u_world;
m4.identity(world);
m4.rotateY(world, time * obj.ySpeed, world);
m4.rotateZ(world, time * obj.zSpeed, world);
m4.transpose(m4.inverse(world, uni.u_worldInverseTranspose), uni.u_worldInverseTranspose);
m4.multiply(viewProjection, uni.u_world, uni.u_worldViewProjection);
gl.useProgram(obj.programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, obj.programInfo, obj.bufferInfo);
// calls gl.bindTexture, gl.activeTexture, gl.uniformXXX
twgl.setUniforms(obj.programInfo, uni);
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, obj.bufferInfo);
});
}
if (true) { // animated
var renderContinuously = function(time) {
render(time);
requestAnimationFrame(renderContinuously);
}
requestAnimationFrame(renderContinuously);
} else {
var requestId;
var renderRequest = function(time) {
render(time);
requestId = undefined;
}
// If animated
var queueRender = function() {
if (!requestId) {
requestId = requestAnimationFrame(renderRequest);
}
}
window.addEventListener('resize', queueRender);
window.addEventListener('scroll', queueRender);
queueRender();
}
* {
box-sizing: border-box;
-moz-box-sizing: border-box;
}
body {
font-family: monospace;
margin: 0;
}
#c {
position: absolute;
top: 0;
width: 100vw;
height: 100vh;
}
#outer {
width: 100%;
z-index: 2;
position: absolute;
top: 0px;
}
#content {
margin: auto;
padding: 2em;
}
#b {
width: 100%;
text-align: center;
}
.list-item {
border: 1px solid black;
margin: 2em;
padding: 1em;
width: 200px;
display: inline-block;
}
.list-item .view {
width: 100px;
height: 100px;
float: left;
margin: 0 1em 1em 0;
}
.list-item .description {
padding-left: 2em;
}
#media only screen and (max-width : 500px) {
#content {
width: 100%;
}
.list-item {
margin: 0.5em;
}
.list-item .description {
padding-left: 0em;
}
}
<script src="//twgljs.org/dist/4.x/twgl-full.min.js"></script>
<body>
<canvas id="c"></canvas>
<div id="outer">
<div id="content">
<div id="b">item list</div>
<div id="list"></div>
</div>
</div>
</body>
<script id="list-item-template" type="notjs">
<div class="view"></div>
<div class="description">Lorem ipsum dolor sit amet, conse ctetur adipi scing elit. </div>
</script>
<script id="vs" type="notjs">
uniform mat4 u_worldViewProjection;
uniform vec3 u_lightWorldPos;
uniform mat4 u_world;
uniform mat4 u_viewInverse;
uniform mat4 u_worldInverseTranspose;
attribute vec4 a_position;
attribute vec3 a_normal;
attribute vec2 a_texcoord;
varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;
void main() {
v_texCoord = a_texcoord;
v_position = (u_worldViewProjection * a_position);
v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
v_surfaceToLight = u_lightWorldPos - (u_world * a_position).xyz;
v_surfaceToView = (u_viewInverse[3] - (u_world * a_position)).xyz;
gl_Position = v_position;
}
</script>
<script id="fs" type="notjs">
precision mediump float;
varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;
uniform vec4 u_lightColor;
uniform vec4 u_diffuseMult;
uniform sampler2D u_diffuse;
uniform vec4 u_specular;
uniform float u_shininess;
uniform float u_specularFactor;
vec4 lit(float l ,float h, float m) {
return vec4(1.0,
abs(l),//max(l, 0.0),
(l > 0.0) ? pow(max(0.0, h), m) : 0.0,
1.0);
}
void main() {
vec4 diffuseColor = texture2D(u_diffuse, v_texCoord) * u_diffuseMult;
vec3 a_normal = normalize(v_normal);
vec3 surfaceToLight = normalize(v_surfaceToLight);
vec3 surfaceToView = normalize(v_surfaceToView);
vec3 halfVector = normalize(surfaceToLight + surfaceToView);
vec4 litR = lit(dot(a_normal, surfaceToLight),
dot(a_normal, halfVector), u_shininess);
vec4 outColor = vec4((
u_lightColor * (diffuseColor * litR.y +
u_specular * litR.z * u_specularFactor)).rgb,
diffuseColor.a);
gl_FragColor = outColor;
}
</script>
If you have a phone you can see a similar one fullscreen here.

svg, keep text from upside down

I have a path in svg, it's original point is on the right, the following points are on the left, then I add a textPath to svg, all text are upside down.
How can I keep the text upright and always show from left to right?
This cost me a lot of time!
I'll appreciate your answer!
<!DOCTYPE html>
<html>
<head>
<style>
</style>
<script>
function CreateSVG(points, container) {
var id = "idSVG";
var ns = "http://www.w3.org/2000/svg";
var xlinkns = "http://www.w3.org/1999/xlink";
var lineWidth = 10;
var color = "rgba(120,120,120,0.7)";
var path = document.createElementNS(ns, "path");
path.setAttribute('stroke-linecap', 'round');
path.setAttribute('stroke-linejoin', 'round');
path.setAttribute('stroke-dasharray', 'solid');
path.setAttribute('fill', 'none');
path.setAttribute('id', id);
var _canvas = document.createElementNS(ns, 'svg');
_canvas.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', xlinkns);
_canvas.setAttribute('pointer-events', 'none');
//this._canvas.appendChild(this.path);
var defs = document.createElementNS(ns, "defs");
defs.appendChild(path);
var useSvg = document.createElementNS(ns, 'use');
useSvg.setAttributeNS(xlinkns, 'xlink:href', '#' + id);
useSvg.setAttribute('fill', 'none');
useSvg.setAttribute('stroke', 'red');
var textSvg = document.createElementNS(ns, 'text');
var textPathSvg = document.createElementNS(ns, 'textPath');
textPathSvg.setAttributeNS(xlinkns, 'xlink:href', '#' + id);
textPathSvg.appendChild(document.createTextNode('test text'));
textSvg.appendChild(textPathSvg);
_canvas.appendChild(defs);
_canvas.appendChild(useSvg);
_canvas.appendChild(textSvg);
var minX, maxX, minY, maxY, x, y, width, height;
for(var i = 0, len = points.length; i < len; i++) {
x = points[i].x;
y = points[i].y;
if(minX == undefined || minX > x) {
minX = x;
}
if(maxX == undefined || maxX < x) {
maxX = x;
}
if(minY == undefined || minY > y) {
minY = y;
}
if(maxY == undefined || maxY < y) {
maxY = y;
}
}
minX = minX - lineWidth;
minY = minY - lineWidth;
width = maxX - minX + lineWidth;
height = maxY - minY + lineWidth;
_canvas.setAttribute('style', 'position:absolute;');
_canvas.setAttribute('width', width);
_canvas.setAttribute('height', height);
_canvas.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
_canvas.setAttribute('stroke', 'true');
path.setAttribute('stroke-width', lineWidth);
path.setAttribute('stroke', color);
var d = [];
for(var i = 0, len = points.length; i < len; i++) {
d[i] = (points[i].x - minX + lineWidth / 2) + ',' + (points[i].y - minY + lineWidth / 2);
}
path.setAttribute('d', 'M' + d.join(' '));
container.appendChild(_canvas);
}
</script>
</head>
<body onload="CreateSVG([{x:200, y:200}, {x:40, y:60}], document.getElementById('svgContainer'))">
<div id="svgContainer"></div>
</body>
</html>

Resources