var PrintPreview = function(jqCanvas) { var canvas = jqCanvas[0]; // === basic wheel reinvention stuff === // function $(id) { return document.getElementById(id) } // comparison function using a key, to pass to .sort() function keycomp(key) { return function(a, b) { var ka = key(a) var kb = key(b) if (ka < kb) return -1 if (ka > kb) return 1 return 0 } } // return a list transformed by a function function map(f, list) { var rv = [] for (var ii = 0; ii < list.length; ii++) rv.push(f(list[ii])) return rv } // === 3d transforms === // We represent transforms as a 3x4 list of lists (ahem, array of arrays): // [[x_from_x, x_from_y, x_from_z, x_off], // [y_from_x, y_from_y, y_from_z, y_off], // [z_from_x, z_from_y, z_from_z, z_off]] // And we only actually multiply points through them in xform. function translate(x, y, z) { return [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z]] } function identity() { return translate(0, 0, 0) } // rotation around the Z-axis function rotate(theta) { var s = Math.sin(theta) var c = Math.cos(theta) return [[c, -s, 0, 0], [s, c, 0, 0], [0, 0, 1, 0]] } // exchange two of the X, Y, Z axes --- useful for making rotate() go around // another axis :) function transpose_axes(a, b) { var rv = identity() var tmp = rv[a] rv[a] = rv[b] rv[b] = tmp return rv } // you'd think we'd have a scale() function too, but I haven't needed it yet. // concatenate two transforms --- the magic that makes it all possible function concat(x1, x2) { var rv = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] for (var ii = 0; ii < 3; ii++) { rv[ii][3] = x2[ii][3] for (var jj = 0; jj < 3; jj++) { rv[ii][3] += x1[jj][3] * x2[ii][jj] for (var kk = 0; kk < 3; kk++) { rv[ii][jj] += x1[kk][jj] * x2[ii][kk] } } } return rv } // concatenate N transforms. I'd insert a special case for 0 transforms, // but amusingly this function isn't all that performance-critical. function concat_n(xforms) { var rv = identity() for (var ii = 0; ii < xforms.length; ii++) rv = concat(rv, xforms[ii]) return rv } // transform a point. function xform(xform, p) { var result_vec = [] for (var ii = 0; ii < 3; ii++) { var rv = xform[ii][3] for (var jj = 0; jj < 3; jj++) rv += xform[ii][jj] * p[jj] result_vec.push(rv) } return result_vec } // transform multiple points. function xform_points(xf, points) { var xp = [] for (var ii = 0; ii < points.length; ii++) { xp.push(xform(xf, points[ii])) } return xp } // perspective-transform a point --- into 2d. function persp(p) { return [p[0] / p[2], p[1] / p[2]] } // perspective-transform multiple points function persp_points(points) { return map(persp, points) } // return the normal of a triangle defined by three points. function normal(p1, p2, p3) { var v1 = [p1[0]-p2[0], p1[1]-p2[1], p1[2]-p2[2]] var v2 = [p2[0]-p3[0], p2[1]-p3[1], p2[2]-p3[2]] var n = [v1[1]*v2[2]-v1[2]*v2[1], v1[2]*v2[0]-v1[0]*v2[2], v1[0]*v2[1]-v1[1]*v2[0]] var mag = Math.sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]) return [n[0]/mag, n[1]/mag, n[2]/mag] } // === 3d shapes === // We represent these as an array of three arrays // [points, lines, polies] where each line is two indices into the points array // and each poly is three indices into the points array function dup(array) { var newarray = new Array(array.length) for (var ii = 0; ii < array.length; ii++) newarray[ii] = array[ii] return newarray } // transform a shape, returning a new shape function xform_shape(xf, shape) { // de-alias new lines and polies return [xform_points(xf, shape[0]), dup(shape[1]), dup(shape[2])] } // add a new shape onto an old shape, mutating the old one function augment(shape1, shape2) { var s1p = shape1[0] var off = s1p.length for (var ii = 0; ii < shape2[0].length; ii++) s1p.push(shape2[0][ii]) var s2ll = shape2[1].length // in case of aliasing for (var ii = 0; ii < s2ll; ii++) shape1[1].push([shape2[1][ii][0] + off, shape2[1][ii][1] + off]) var s2pl = shape2[2].length for (var ii = 0; ii < s2pl; ii++) { var tri = shape2[2][ii] shape1[2].push([tri[0]+off, tri[1]+off, tri[2]+off]) } } // given a shape, make a more complicated shape by copying it through transform // xf n times, and connecting the corresponding points. This is more powerful // than the usual kind of extrusion, and can be used to create fairly // interesting shapes --- a snail shell from a circle, for instance. function extrude_shape(xf, shape, n) { if (n == null) n = 1 var new_part = shape var old_line_base = 0 // where the lines to attach the triangles start for (var ii = 0; ii < n; ii++) { var new_part = xform_shape(xf, new_part) var shape_length = shape[0].length var new_line_base = shape[1].length // for triangles later augment(shape, new_part) var new_part_length = new_part[0].length // connect corresponding points for (var jj = 0; jj < new_part_length; jj++) { shape[1].push([shape_length + jj - new_part_length, shape_length + jj]) } // make triangles var nlines = new_part[1].length // var old_line_base = new_line_base - nlines for (var jj = 0; jj < nlines; jj++) { var old_line = shape[1][old_line_base + jj] var new_line = shape[1][new_line_base + jj] shape[2].push([old_line[0], old_line[1], new_line[0]]) shape[2].push([new_line[1], new_line[0], old_line[1]]) } old_line_base = new_line_base } } // a shape consisting of a single point function point_shape(x, y, z) { return [[[x, y, z]], [], []] } // approximate a circle in the x-y plane around the origin; radius r and n points function circle(r, n) { var shape = point_shape(r, 0, 0) extrude_shape(rotate(Math.atan(1)*8/n), shape, n) return shape } // approximate a torus with major radius r2 and minor radius r1, // with correspondingly n2 and n1 points around each axis function make_torus(r1, r2, n1, n2) { var c = xform_shape(translate(r2, 0, 0), circle(r1, n1)) extrude_shape(concat_n([transpose_axes(1, 2), rotate(Math.atan(1)*8/n2), transpose_axes(1, 2)]), c, n2) return c } // === drawing code === // draw a 3d shape on a canvas // 95% of the run time is in this function and its kids function draw_shape(canvas, xf, shape, alpha) { var ctx = canvas.getContext('2d') var w = canvas.width var h = canvas.height // set up coordinate system so canvas is (-1, -1) to (1, 1) ctx.save() ctx.translate(w/2, h/2) ctx.scale(w/2, h/2) // 1/3 of the time is in these two lines (when not doing polies) var points3d = xform_points(xf, shape[0]) var points = persp_points(points3d) var lines = shape[1] // 2/3 of the time is in this loop (when we're not doing polies) if (alpha == null) { ctx.strokeStyle = 'grey' ctx.lineWidth = 1/(w/2) ctx.beginPath() var p1, p2 for (var ii = 0; ii < lines.length; ii++) { p1 = points[lines[ii][0]] p2 = points[lines[ii][1]] ctx.moveTo(p1[0], p1[1]) ctx.lineTo(p2[0], p2[1]) } ctx.stroke() } // when we're doing polies, 90% of our time is spent doing polies if (alpha != null) { // Sort polygons by depth so we draw the farthest-away stuff first // ("painter's algorithm") var minusdepth = function(p) { return [-(points3d[p[0]][2] + points3d[p[1]][2] + points3d[p[2]][2]), p] } var polies = map(minusdepth, shape[2]) polies.sort(keycomp(function(p) { return p[0] })) // draw all the polygons var tri, p1, p2, p3, n, bright for (var ii = 0; ii < polies.length; ii++) { tri = polies[ii][1] if (alpha == '1') { // light surface n = normal(points3d[tri[0]], points3d[tri[1]], points3d[tri[2]]) // I'm not sure how to make backface removal work with perspective: // if (n[2] > 0 && alpha == '1') continue // backface removal // lighting from (1, -1, -1) direction bright = parseInt(((n[0]-n[1]-n[2]) / Math.sqrt(3) * 255)) if (bright < 20) bright = 20 } else { // lighting doesn't make sense if the object is transparent, // so we color by depth to have some variation in color... var maxd = polies[polies.length-1][0] var mind = polies[0][0] var d = polies[ii][0] bright = parseInt((d-mind)/(maxd-mind) * 255) } ctx.fillStyle = 'rgba(' + bright + ',' + bright + ',' + bright + ',' + alpha + ')'; ctx.beginPath() p1 = points[tri[0]] p2 = points[tri[1]] p3 = points[tri[2]] ctx.moveTo(p1[0], p1[1]) ctx.lineTo(p2[0], p2[1]) ctx.lineTo(p3[0], p3[1]) // ctx.closePath() seems to be unnecessary ctx.fill() } } ctx.restore() } // clear a canvas function cls(canvas) { var ctx = canvas.getContext('2d') ctx.fillStyle = 'black' ctx.fillRect(0, 0, canvas.width, canvas.height) } // // === drawing of particular shapes. also DOM. === // angle = 0 // function unit_cube() { // var shape = point_shape(0, 0, 0) // extrude_shape(translate(0,0,1), shape) // extrude_shape(translate(0,1,0), shape) // extrude_shape(translate(1,0,0), shape) // return shape // } // // this was where I tested stuff as I wrote this // function make_some_junk() { // // make a unit cube centered on the origin // var shape = xform_shape(translate(-0.5, -0.5, -0.5), unit_cube()) // // add some circles // augment(shape, circle(0.707, 16)) // augment(shape, xform_shape(transpose_axes(0, 2), circle(0.707, 16))) // augment(shape, xform_shape(transpose_axes(1, 2), circle(0.707, 16))) // augment(shape, circle(1, 15)) // // add a disc // var big_disc = circle(2, 20) // extrude_shape(translate(0, 0, 0.5), big_disc, 2) // augment(shape, big_disc) // return shape // } // var some_junk = make_some_junk() // function draw_some_junk(canvas) { // var xf = concat_n([transpose_axes(1, 2), // rotate(angle), // transpose_axes(1, 2), // rotate(angle*1.618), // translate(0, 0, 2.5)]) // draw_shape(canvas, xf, some_junk) // } var torus = make_torus(1, 3, 12, 12) console.log('torus',torus); function draw() { // var start = new Date() // var alpha = null // if ($('fill').checked) { // alpha = ($('translucent').checked ? '0.5' : 1) // } // if ($('trails').checked) { // $('canvas').getContext('2d').globalAlpha = 0.33 // } else { // $('canvas').getContext('2d').globalAlpha = 1 // } var angle = 0; var alpha = 1; draw_shape(canvas, concat_n([ rotate(0), transpose_axes(1, 2), rotate(0), // to reduce periodicity transpose_axes(1, 2), translate(0, 0, 6), // transpose_axes(1, 2), // rotate(1), ]), torus, alpha); // var end = new Date() // var ms = $('ms') // if (ms) { // var msvalue = ms.value + ' ' + (end.getTime() - start.getTime()) // if (msvalue.length > 25) msvalue = msvalue.substr(msvalue.length - 25) // ms.value = msvalue // } } // function update() { // if (!$('go').checked) return // angle += 3.14159 / 30 // cls($('canvas')) // draw_torus($('canvas')) // } // function init(ev) { // setInterval(update, 100) // // this doesn't work: $('fill').addEventListener('change', update, true) // // how do you do what I want to do there? // cls($('canvas')) // draw_torus($('canvas')) // } // window.addEventListener('load', init, true) return { draw: draw, } }