/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ "use strict"; let gs_kiri_sla = exports; (function() { if (!self.kiri) self.kiri = { }; if (!self.kiri.driver) self.kiri.driver = { }; if (self.kiri.driver.SLA) return; let KIRI = self.kiri, BASE = self.base, DBUG = BASE.debug, UTIL = BASE.util, CONF = BASE.config, POLY = BASE.polygons, SLA = KIRI.driver.SLA = { slice, sliceRender, printSetup, printExport, printDownload, printRender }, SLICER = KIRI.slicer, newTop = KIRI.newTop, newSlice = KIRI.newSlice, newPoint = BASE.newPoint, newPolygon = BASE.newPolygon, preview, previewSmall, previewLarge, fill_cache; /** * DRIVER SLICE CONTRACT - runs in worker * * @param {Object} settings * @param {Widget} Widget * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ function slice(settings, widget, onupdate, ondone) { let process = settings.process, device = settings.device, work_total, work_remain; if (!self.OffscreenCanvas) { return ondone("browser lacks support for OffscreenCanvas",true); } // calculate % complete and call onupdate() function doupdate(work, msg) { onupdate(0.25 + ((work_total - work_remain) / work_total) * 0.75, msg); work_remain -= work; } // for each slice, perform a function and call doupdate() function forSlices(slices, work, fn, msg) { slices.forEach(function(slice,index) { fn(slice,index); doupdate(work / slices.length, msg) }); } let b64 = atob(currentSnap); let bin = Uint8Array.from(b64, c => c.charCodeAt(0)); let img = new png.PNG().parse(bin, (err, data) => { preview = img; previewSmall = samplePNG(img, 200, 125); previewLarge = samplePNG(img, 400, 300); }); let height = process.slaSlice || 0.05; SLICER.sliceWidget(widget, { height: height, add: !process.slaOpenTop }, function(slices) { // hold onto last (empty) slice let last = slices[slices.length-1]; // remove empty slices slices = widget.slices = slices.filter(slice => slice.tops.length); if (!process.slaOpenTop) { // re-add last empty slice for open top slices.push(last); } // prepend raft layers to slices array if (process.slaSupportEnable && process.slaSupportLayers) { let layers = process.slaSupportLayers, zoff = height / 2, snew = [], polys = [], gap = process.slaSupportGap, // gap layers above raft grow = height, // union per layer expand off = 1 - (layers * grow); // starting union offset from part let outer = slices.forEach(slice => { // poly.clone prevents inner voids from forming polys.appendAll(slice.tops.map(t => t.poly.clone())); }); // p.clone prevents inner voids from forming let union = POLY.union(polys).map(p => p.clone()); let expand = POLY.expand(union, off, zoff, [], 1); let lastraft; for (let s=0; s { slice.tops.push(newTop(u.clone(true).setZ(zoff))); }); expand = POLY.expand(expand, grow, zoff, [], 1); lastraft = slice; } snew.push(slice); zoff += height; } // compensate for midline start zoff -= height / 2; // replace slices with new appended array slices = widget.slices = snew.concat(slices.map(s => { s.tops.forEach(t => t.poly.setZ(s.z + zoff)); s.index += snew.length; s.z += zoff; return s; })); // annotate widget for support generation widget.union = union; widget.lastraft = lastraft; } // re-connect slices into linked list for island/bridge projections for (let i=1; i { return t+v }); work_remain = work_total; forSlices(slices, 5, (slice,index) => { if (process.slaShell) { slice.doShells(2, 0, process.slaShell); } else { slice.doShells(1, 0); } }, "slice"); forSlices(slices, 10, (slice) => { if (slice.synth) return; slice.doDiff(0.00001, 0.005, !process.slaOpenBase); }, "delta"); if (solidLayers) { forSlices(slices, 10, (slice) => { if (slice.synth) return; slice.projectFlats(solidLayers); slice.projectBridges(solidLayers); }, "project"); forSlices(slices, 10, (slice) => { if (slice.synth) return; slice.doSolidsFill(undefined, undefined, 0.001); let traces = POLY.nest(POLY.flatten(slice.gatherTraces([]))); let trims = slice.solids.trimmed || []; traces.appendAll(trims); let union = POLY.union(traces); slice.solids.unioned = union; }, "solid"); } else { forSlices(slices, 10, (slice) => { if (slice.synth) return; slice.solids.unioned = slice.gatherTopPolys([]); }, "solid"); } if (process.slaFillDensity && process.slaShell) { fill_cache = []; forSlices(slices, 60, (slice) => { if (slice.synth) return; fillPolys(slice, settings); }, "infill"); } if (process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity) { computeSupports(widget, process, progress => { doupdate(100 * progress, "support"); }); } ondone(); }, function(update) { return onupdate(0.0 + update * 0.25); }); }; function computeSupports(widget, process, progress) { let area = widget.union.reduce((t,p) => { return t + p.areaDeep() }, 0), perim = widget.union.reduce((t,p) => { return t + p.perimeter() }, 0), slices = widget.slices, length = slices.length, tot_mass = 0, // total widget "mass" tot_bear = 0; // total "mass-bearing" area let ops = slices.filter(slice => !slice.synth); // compute total "mass" by slice ops.forEach(slice => { slice.mass = slice.solids.unioned.reduce((t,p) => { return t + p.areaDeep() }, 0); if (slice.up && slice.up.bridges) { slice.bear = slice.up.bridges.reduce((t,p) => { return t + p.areaDeep() }, 0); slice.bear_up = slice.up.bridges; tot_bear += slice.bear; } else { slice.bear = 0; } tot_mass += slice.mass; }); let mass_per_bear = (tot_mass / tot_bear) * (1 / process.slaSupportDensity); // console.log({tot_mass, tot_bear, ratio: tot_mass / tot_bear, mass_per_bear}); let first; ops.slice().map((s,i) => { if (!first && s.bear) { s.ord_first = first = true; } // 30x lowest to 1x highest s.ord_weight = Math.pow(30,((ops.length - i) / ops.length)); return s; }).sort((a,b) => { if (a.ord_first) return -1; if (b.ord_first) return 1; return (b.bear * b.ord_weight) - (a.bear * a.ord_weight); }).forEach((slice, index) => { slice.ord_bear = index; }) let rem_mass = tot_mass, rem_bear = tot_bear; // compute remaining mass, bearing surface, for each slice let run, runLast, runCount = 0, runList = []; ops.forEach(slice => { slice.rem_mass = rem_mass; slice.rem_bear = rem_bear; // slice.can_bear = slice.mass * mass_per_bear; slice.can_bear = slice.bear * mass_per_bear; rem_mass -= slice.mass; rem_bear -= slice.bear; }); let ord = ops.sort((a,b) => { return a.ord_bear - b.ord_bear; }); rem_mass = tot_mass; rem_bear = tot_bear; // in order of load bearing capability, select layer // and recompute the requirements on the slices below for (let i=0; iops[0].index; j--) { slices[j].rem_mass -= bearing; } // remove total mass left to bear rem_mass -= bearing; slice.can_emit = true; if (rem_mass <= 0) { // console.log({break: i, of: ord.length}); break; } } let seq = 0, seqLast = 0, spacing = (1 - process.slaSupportDensity) * 10, size = Math.bound(process.slaSupportSize / 2, 0.25, 1); // compute and project support pillars slices.forEach((slice,index) => { if (slice.can_emit) { if (seq === 0 || slice.index - seqLast > 5) { slice.bear_up.map(p => { return p.clone(true).setZ(slice.z); }).forEach(p => { projectSupport(process, slice, p, size, spacing); }); seq++; } else if (seq > 5) { seq = 0; } else { seq++; } seqLast = slice.index; } progress(1 / slices.length); }); // union support pillars slices.forEach(slice => { if (slice.supports) { slice.supports = POLY.union(slice.supports,0); } }); } function projectSupport(process, slice, poly, size, spacing) { let flat = poly.circularityDeep() > 0.1, arr = [ poly ]; // insetting polys produces arrays. consume til gone while (arr.length) { poly = arr.shift(); let out = [], seg = [], per = poly.perimeter(), crit = 0, polys = poly.clone(true).flattenTo([]); polys.forEach(p => p.forEachSegment((p1, p2) => { let rec = {dist: p1.distTo2D(p2), p1, p2}; if (rec.dist >= spacing || slice.ord_first) crit++; seg.push(rec); })); seg.sort((a,b) => { return b.dist - a.dist; }); // emit all critical (long) segments. min 3 while (seg.length && (crit > 0 || out.length < 3)) { let {dist, p1, p2} = seg.shift(); if (dist >= spacing) { // spaced along line let num = Math.ceil(dist / spacing) + 1, step = dist / num, pt = p1; while (num-- >= 0) { out.push(pt); pt = pt.offsetPointTo(p2, step); } crit--; } else { // line midpoint out.push(p1.offsetPointTo(p2, dist / 2)); } } // drop points too close to other pillars if (!slice.ord_first) drop: for (let i=0; i { if (out[i] && out[i].distTo2D(rec.point) <= size) { out[i] = null; } }); } // mark support pillar for each point out.filter(p => p !== null) .forEach(p => { let track = projectPillar(process, slice, p, size, []); // find stunted pillars terminating on a face (not raft or merged) if (track.length && !(track.max || track.synth || track.merged)) { // remove stunted pillar track.forEach(rec => { let sp = rec.slice.supports; let rp = sp.indexOf(rec.pillar); if (rp >= 0) sp.splice(rp,1); }); } }); // inset polygon for flat area support if (flat) poly.offset(Math.max(0.2, 1 - process.slaSupportDensity), arr); } } function projectPillar(process, slice, point, size, track) { if (!slice.supports) slice.supports = []; if (!slice.pillars) slice.pillars = []; let points = process.slaSupportPoints, pillar = newPolygon() .centerCircle(point, size/2, points, true) .setZ(slice.z), max = process.slaSupportSize, inc = process.slaSlice/2, end = false, // center intersects over = [], // overlapping points safe = [], // non-overlapping points low = slice.index < process.slaSupportGap + process.slaSupportLayers; track.min = track.min ? Math.min(track.min, size) : size; track.max = (track.max ? true : false) || size >= max; track.synth = track.synth || slice.synth; slice.tops.forEach(t => { if (track.length > 3 && point.isInPolygon(t.poly)) { end = true; } pillar.points.forEach(p => { let isin = p.isInPolygon(t.poly);// || p.nearPolygon(t.poly, 0.001); if (isin) { over.push(p); } else { safe.push(p); } }); }); if (end) { // backtrack shrinking pillars to point if // not landing on the base and size is max'd if (!slice.synth && track.max) { let nusize = track.min; while (track.length) { let prec = track.pop(), npil = newPolygon() .centerCircle(prec.point, nusize/2, points, true) .setZ(prec.slice.z), spos = prec.slice.supports.indexOf(prec.pillar); // if we find our old pillar, replace if (spos >= 0) { prec.slice.supports[spos] = npil; nusize += inc; } if (nusize > prec.size) { break; } } } return track; } let nextpoint = point; if (over.length) { // move toward average of safe (non-overlapping) points if (safe.length) { let x = 0, y = 0; safe.forEach(p => { x += p.x; y += p.y }); x /= safe.length; y /= safe.length; nextpoint = point.offsetPointTo({x, y, z:slice.z}, inc); } // once max'ed out, can only shrink in size if (track.max) { size -= inc; } } else if (!track.max || low) { size += inc; if (low) max += (process.slaSupportGap * process.slaSlice * 2); } if (size < track.min) { return track; } let close = []; for (let i=0; i { newp.x += p.x; newp.y += p.y; }); newp.x /= (close.length + 1); newp.y /= (close.length + 1); nextpoint = point.offsetPointTo(newp, inc); } slice.supports.push(pillar); slice.pillars.push({point, pillar, size}); track.push({slice, point, pillar, size}); if (slice.down) { projectPillar(process, slice.down, nextpoint, Math.min(size, max), track); } return track; } function fillPolys(slice, settings) { let process = settings.process, device = settings.device, polys = slice.solids.unioned, bounds = settings.bounds, width = bounds.max.x - bounds.min.x, depth = bounds.max.y - bounds.min.y, max = Math.max(width,depth), seq = Math.round(process.slaFillLine / process.slaSlice), linew = process.slaFillLine, units_w = (width / linew) * process.slaFillDensity, units_d = (depth / linew) * process.slaFillDensity, step_x = width / units_w, step_y = depth / units_d, start_x = -(width / 2), start_y = -(depth / 2), end_x = width / 2, end_y = depth / 2, fill = []; let seq_i = Math.floor(slice.index / seq), seq_c = seq_i % 4, cached = fill_cache[seq_c]; if (!cached && seq_c !== 1) for (let x=start_x; x p.clone(true).setZ(slice.z)); } fill = POLY.trimTo(fill, slice.tops.map(t => t.poly)); fill = POLY.union(slice.solids.unioned.appendAll(fill)); slice.solids.unioned = fill; } /** * DRIVER PRINT CONTRACT - runs in worker * @param {Object} print state object * @param {Function} update incremental callback */ function printSetup(print, update) { update(1); }; /** * DRIVER PRINT CONTRACT - runs in worker * @param {Object} print state object * @param {Function} online streaming reply * @param {Function} ondone last reply */ function printExport(print, online, ondone) { let widgets = print.widgets, settings = print.settings, device = settings.device, process = settings.process, output = print.output, images = [], layermax = 0, width = 2560, height = 1440, width2 = width/2, height2 = height/2, scaleX = width / device.bedWidth, scaleY = height / device.bedDepth, mark = Date.now(); // find max layer count widgets.forEach(widget => { layermax = Math.max(widget.slices.length); }); function polyout(poly, ctx) { poly.forEachPoint((p,i) => { if (i === 0) { ctx.moveTo(height - (p.y * scaleY + height2), p.x * scaleX + width2); } else { ctx.lineTo(height - (p.y * scaleY + height2), p.x * scaleX + width2); } }, true); ctx.closePath(); } // generate layer 8-bit bitaps using canvas for (let index=0; index < layermax; index++) { let layer = new OffscreenCanvas(height,width); let ctx = layer.getContext('2d'); ctx.fillStyle = 'rgb(200, 0, 0)'; let count = 0; widgets.forEach(widget => { let slice = widget.slices[index]; if (slice) { // prevent premature exit on empty synth slice if (slice.synth) count++; let polys = slice.solids.unioned; if (!polys) polys = slice.tops.map(t => t.poly); if (slice.supports) polys.appendAll(slice.supports); polys.forEach(poly => { poly.move(widget.track.pos); ctx.beginPath(); polyout(poly.setClockwise(), ctx); if (poly.inner) { poly.inner.forEach(inner => { polyout(inner.setCounterClockwise(), ctx); }); } ctx.fill(); count++; }); } else { // console.log({no_slice_at: index}) } }); let data = ctx.getImageData(0,0,height,width).data; // reduce RGBA to R let red = new Uint8ClampedArray(data.length / 4); for (let i=0; i { online({progress: progress * 0.75 + 0.25, message}); }); ondone({ width: width, height: height, file: file },[file]); console.log('print.export', Date.now() - mark); }; // runs in browser main function sliceRender(widget) { widget.slices.forEach(slice => { let layers = slice.layers, outline = layers.outline, support = layers.support; if (slice.solids.unioned) { // console.log('solid', slice.index) slice.solids.unioned.forEach(poly => { poly = poly.clone(true);//.move(widget.track.pos); outline.poly(poly, 0x010101, true); outline.solid(poly, 0x0099cc); }); } else if (slice.tops) { // console.log('top', slice.index) slice.tops.forEach(top => { let poly = top.poly;//.clone(true).move(widget.track.pos); outline.poly(poly, 0x010101, true, false); outline.solid(poly, 0xfcba03); }); } if (slice.supports) { // console.log('support', slice.index) slice.supports.forEach(poly => { //poly = poly.clone(true).move(widget.track.pos); support.poly(poly, 0x010101, true, false); support.solid(poly, 0xfcba03); }); } slice.renderDiff(); slice.renderSolidOutlines(); outline.renderAll(); support.renderAll(); }); } // runs in browser main function printRender(print) { let widgets = print.widgets, settings = print.settings, device = settings.device, process = settings.process; for (let index=0; ; index++) { let layer = KIRI.newLayer(print.group); let count = 0; widgets.forEach(widget => { let slice = widget.slices[index]; if (!slice) { return; } count++; let polys = slice.solids.unioned; if (!polys) polys = slice.tops.map(t => t.poly); if (slice.supports) polys.appendAll(slice.supports); polys.forEach(poly => { poly = poly.clone(true).move(widget.track.pos); layer.poly(poly, 0x010101, true); layer.solid(poly, 0x0099cc); }); }); layer.renderSolid(); layer.render(); if (count === 0) { // TODO fix with contract for exposing layer count // hack uses expected gcode output array in print object // print.output = print.printView; return; } print.printView.push(layer); } } // runs in browser main function printDownload(print) { let { API, lines, done } = print.sla; let filename = `print-${new Date().getTime().toString(36)}`; API.ajax("/kiri/output-sla.html", html => { API.ui.print.innerHTML = html; let printset = print.settings, process = printset.process, device = printset.device, print_sec = (process.slaBaseLayers * process.slaBaseOn) + (lines.length - process.slaBaseLayers) * process.slaLayerOn; // add peel lift/drop times to total print time for (let i=0; i { saveFile(API, done.file, ".photon") }; break; case 'Anycubic.Photon.S': download.innerText += " .photons"; download.onclick = () => { saveFile(API, done.file, ".photons") }; break; } let canvas = $('print-canvas'); let ctx = canvas.getContext('2d'); let img = ctx.createImageData(done.height, done.width); let imgDV = new DataView(img.data.buffer); let range = $('print-range'); range.value = 0; range.min = 0; range.max = lines.length - 1; range.oninput = function() { let lineDV = new DataView(lines[range.value].buffer); for (let i=0; i { let count = line.length; let lineDV = new DataView(line.buffer); let bits = new Uint8Array(line.length); let bitsDV = new DataView(bits.buffer); let subs = [{ data: bits, view: bitsDV }]; for (let sl=1; sl { progress(pro * 0.4 + 0.4, "layer_encode"); })); let buflen = 3000 + coded.length + (layerCount * subcount * 28) + small.byteLength + large.byteLength; let filebuf = new ArrayBuffer(buflen); let filedat = new DataWriter(new DataView(filebuf)); let printtime = (process.slaBaseLayers * process.slaBaseOn) + (coded.layers.length - process.slaBaseLayers) * process.slaLayerOn; filedat.writeU32(0x1900fd12); // header filedat.writeU32(2,true); // version filedat.writeF32(68.04, true); // bed x filedat.writeF32(120.96, true); // bed y filedat.writeF32(150.0, true); // bed z filedat.skip(12); // padding filedat.writeF32(process.slaSlice, true); // layer height filedat.writeF32(process.slaLayerOn, true); // default lamp on filedat.writeF32(process.slaBaseOn, true); // base lamp on filedat.writeF32(process.slaLayerOff, true); // lamp off filedat.writeU32(process.slaBaseLayers, true); // base layers filedat.writeU32(1440, true); // device x filedat.writeU32(2560, true); // device y let hirez = filedat.skip(4); // hirez preview address filled pater let layerpos = filedat.skip(4); // layer data address filled later filedat.writeU32(layerCount, true); let lorez = filedat.skip(4); // hirez preview address filled later filedat.writeU32(printtime, true); // print time seconds filedat.writeU32(1, true); // projection type (1=lcd, 0=cast) let proppos = filedat.skip(4); // print properties address filled later let proplen = filedat.skip(4); // print properties length filled later filedat.writeU32(subcount, true); // AA level (sub layers) filedat.writeU16(0x00ff, true); // light pwm (TODO); filedat.writeU16(0x00ff, true); // light pwm bottom (TODO); let propstart = filedat.pos; filedat.view.setUint32(proppos, filedat.pos, true); // write print properties filedat.writeF32(process.slaBasePeelDist, true); filedat.writeF32(process.slaBasePeelLiftRate * 60 , true); filedat.writeF32(process.slaPeelDist, true); filedat.writeF32(process.slaPeelLiftRate * 60 , true); filedat.writeF32(process.slaPeelDropRate * 60, true); filedat.writeF32(0, true); // volume of used filedat.writeF32(0, true); // weight of used filedat.writeF32(0, true); // cost of used filedat.writeF32(0, true); // bottom off delay time filedat.writeF32(0, true); // light off delay time filedat.writeU32(process.slaBaseLayers, true); filedat.writeF32(0, true); // p1 ? filedat.writeF32(0, true); // p2 ? filedat.writeF32(0, true); // p3 ? filedat.writeF32(0, true); // p4 ? filedat.view.setUint32(proplen, filedat.pos - propstart, true); filedat.view.setUint32(layerpos, filedat.pos, true); // write layer headers let layers = coded.layers; let layerat = []; for (let sc=0; sc { let count = line.length / 4; let bits = new Uint8Array(line.length / 4); let bitsDV = new DataView(bits.buffer); let lineDV = new DataView(line.buffer); // reduce RGB to R = 0||1 for (let i = 0; i < count; i++) { // defeat anti-aliasing for the moment bitsDV.setUint8(i, lineDV.getUint8(i * 4) > 0 ? 1 : 0); } progress(index / conf.lines.length); return { subs: [{ exposureTime: process.slaLayerOn, data: bits }] }; }); let coded = encodeLayers(converted, "photons"); let filebuf = new ArrayBuffer(75366 + coded.length + 28 * layerCount); let filedat = new DataView(filebuf); let filePos = 0; filedat.setUint32 (0, 2, false); filedat.setUint32 (4, 3227560, false); filedat.setUint32 (8, 824633720, false); filedat.setUint16 (12, 10, false); filedat.setFloat64(14, process.slaSlice, false); filedat.setFloat64(22, process.slaLayerOn, false); filedat.setFloat64(30, process.slaLayerOff, false); filedat.setFloat64(38, process.slaBaseOn, false); filedat.setUint32 (46, process.slaBaseLayers, false); filedat.setFloat64(50, process.slaPeelDist, false); filedat.setFloat64(58, process.slaPeelLift, false); filedat.setFloat64(66, process.slaPeelDrop, false); filedat.setFloat64(74, 69420, false); filedat.setUint32 (82, 224, false); filedat.setUint32 (86, 42, false); filedat.setUint32 (90, 168, false); filedat.setUint32 (94, 10, false); filedat.setUint32 (75362, layerCount, false); filePos = 75366; for (let i = 0; i < layerCount; i++) { let layer = coded.layers[i], sublayer = layer.sublayers[0], numbytes = sublayer.length; filedat.setUint32 (filePos + 0, 69420, false); filedat.setFloat64(filePos + 4, 0); filedat.setUint32 (filePos + 12, height, false); filedat.setUint32 (filePos + 16, width, false); filedat.setUint32 (filePos + 20, numbytes * 8 + 32, false); filedat.setUint32 (filePos + 24, 2684702720, false); filePos += 28; for (let j = 0; j < numbytes; j++) { filedat.setUint8(filePos + j, sublayer[j], false); } filePos += numbytes; progress((i / layerCount) / 2 + 0.5); } return filebuf; } function encodeLayers(input, type, progress) { let layers = [], length = 0, total = 0, count = 0; input.forEach(layer => { layer.subs.forEach(sub => total++); }); for (let index = 0; index < input.length; index++) { let subs = input[index].subs, sublayers = [], sublength = 0; for (let subindex = 0; subindex < subs.length; subindex++) { let data = subs[subindex].data; let encoded = rleEncode(data, type); sublength += encoded.length; sublayers.push(encoded); if (progress) progress(count++/total); if (type == "photons") break; } length += sublength; layers.push({ sublength, sublayers }); } return { length, layers }; } function rleEncode(data, type) { let maxlen = (type === 'photons') ? 128 : 125, color = data[0], runlen = 1, output = []; for (let index = 1; index < data.length; index++) { let newColor = data[index]; if (newColor !== color) { output.push(rleByte(color, runlen, type)); color = newColor; runlen = 1; } else { if (runlen === maxlen) { output.push(rleByte(color, runlen, type)); runlen = 1; } else { runlen++; } } } if (runlen > 0) { output.push(rleByte(color, runlen, type)); } return output; } function rleByte(color, length, type) { switch (type) { case 'photon': return (length & 0x7f) | ((color << 7) & 0x80); case 'photons': length--; return (length & 1 ? 128 : 0) | (length & 2 ? 64 : 0) | (length & 4 ? 32 : 0) | (length & 8 ? 16 : 0) | (length & 16 ? 8 : 0) | (length & 32 ? 4 : 0) | (length & 64 ? 2 : 0) | color; } } function rleDecode(data, type) { let bytes = []; if (type === 'photon') { for (let i = 0; i < data.length; i++) { let val = data[i], color = val >> 7, count = val & 0x7f; for (let j = 0; j < count; j++) { bytes.push(color); } } } else { for (let i = 0; i < data.length; i++) { let val = data[i], color = val & 1, count = ((val & 128 ? 1 : 0) | (val & 64 ? 2 : 0) | (val & 32 ? 4 : 0) | (val & 16 ? 8 : 0) | (val & 8 ? 16 : 0) | (val & 4 ? 32 : 0) | (val & 2 ? 64 : 0)) + 1; for (let j = 0; j < count; j++) { bytes.push(color); } } } return bytes; } function pixAt(png,x,y) { let idx = (x + png.width * y) * 4; let dat = png.data; return [ dat[idx++], dat[idx++], dat[idx++], dat[idx++] ]; } function averageBlock(png,x1,y1,x2,y2) { let val = [0, 0, 0, 0], count = 0, x, y, z, v2; for (x=x1; x 4/3) { div = png.height / tw; xoff = Math.round((png.width - (th * div)) / 2); yoff = 0; } else { div = png.width / th; xoff = 0; yoff = Math.round((png.height - (tw * div)) / 2); } for (let y=0; y png.height) continue; ey = Math.round((y+1) * div + yoff); for (let x=0; x png.width) continue; ex = Math.round((x+1) * div + xoff); bidx = (y * th + x) * 4; pixval = averageBlock(png,dx,dy,ex,ey); buf[bidx+0] = pixval[0]; buf[bidx+1] = pixval[1]; buf[bidx+2] = pixval[2]; buf[bidx+3] = pixval[3]; } } return {width, height, data:buf, png}; } function writePhotonImage(preview, writer) { let data = new Uint8Array(preview.data), len = data.byteLength; writer.writeU32(preview.width, true); writer.writeU32(preview.height, true); let hpos = writer.skip(4); writer.writeU32(len/2, true); writer.view.setUint32(hpos, writer.pos, true); let pos = 0; while (pos < len) { let r = data[pos++], g = data[pos++], b = data[pos++], a = data[pos++], v = (((r/4)&0x1f) << 11) | (((g/4)&0x1f) << 6) | (((b/4)&0x1f) << 0) ; writer.writeU16(v, true); } } })(); class DataWriter { constructor(view) { this.view = view; this.pos = 0; } skip(v) { let p = this.pos; this.pos += v; return p; } writeU8(v) { try { this.view.setUint8(this.pos, v); return this.skip(1); } catch (err) { console.log({pos:this.pos, err}) throw err; } } writeU16(v,le) { this.view.setUint16(this.pos, v, le); return this.skip(2); } writeU32(v,le) { this.view.setUint32(this.pos, v, le); return this.skip(4); } writeF32(v,le) { this.view.setFloat32(this.pos, v, le); return this.skip(4); } writeF64(v,le) { this.view.setFloat64(this.pos, v, le); return this.skip(8); } }