UNPKG

wacomink

Version:
1,737 lines (1,418 loc) 75 kB
/** * @namespace Module * * @description connect to canvas */ /* For local usagse: Firefox: "security.fileuri.strict_origin_policy" should be false, parameter can be found in about:config Chrome: start with parameter "--allow-access-from-files" */ var Module = { preRun: [], postRun: [], addPostScript: function(callback) { this.postRun.unshift(callback); }, print: function(text) { if (text && text != "") console.log(text); }, printErr: function(text) { text = Array.prototype.slice.call(arguments).join(" "); if (0) // XXX disabled for safety typeof dump == "function") { dump(text + "\n"); // fast, straight to the real console else console.log(text); }, canvas: null, canvasID: "canvas", // setStatus: function(text) {}, totalDependencies: 0, monitorRunDependencies: function(left) { this.totalDependencies = Math.max(this.totalDependencies, left); // Module.setStatus(left?"Preparing... (" + (this.totalDependencies-left) + "/" + this.totalDependencies + ")":"All downloads complete."); } }; Module.getScriptPrefixURL = function(name) { var result = null; var scripts = document.getElementsByTagName("script"); for (var i = 0; i < scripts.length; i++) { if (scripts[i].src.contains(name)) { var src = scripts[i].getAttribute("src"); if (src.contains("/")) src = src.substring(0, src.lastIndexOf("/")); else src = ""; result = src; break; } } return result; } if (ENVIRONMENT == EnvironmentType.WEB) { /** * Prefix path to the engine mem file. * Auto initialized with prefix path of Module.js. * Should be configured manually when used in workers or for custom mem file location. * * @memberof Module * @member {String} memoryInitializerPrefixURL */ Module.memoryInitializerPrefixURL = Module.getScriptPrefixURL("Module.js") + "/"; } Module.addPostScript(function() { Object.defineProperty(Module.VectorInterval.prototype, "length", {get: function() {return this.size();}}); Object.defineProperty(Module.VectorInt64Ptr.prototype, "length", {get: function() {return this.size();}}); Object.defineProperty(Module.PathBuilder.prototype, "stride", {get: function() {return this.calculateStride();}}); Object.extend(Module.VectorInterval.prototype, { push: Module.VectorInterval.prototype.push_back, forEach: function(callback, context) { for (var i = 0; i < this.size(); i++) callback.call(context || {}, this.get(i), i, this); }, toArray: function() { var result = new Array(); for (var i = 0; i < this.size(); i++) result.push(this.get(i)); return result; } }); Module.VectorInt64Ptr.prototype.forEach = Module.VectorInterval.prototype.forEach; Object.extend(Module.PathBuilder, { createPath: function(points, stride) { if (Array.isArray(points)) points = points.toFloat32Array(); return {points: points, stride: stride}; } }); Object.extend(Module.PathBuilder.prototype, { createPath: function(points) { this.super.createPath.apply(this.super, arguments); return Module.PathBuilder.createPath(points, this.stride); }, addPathPart: function(pathPart) { this.super.addPathPart.apply(this.super, arguments); var pathContext; Module.writeBytes(pathPart.points, function(points) { pathContext = this.nativeAddPathPart(points); }, this); pathContext.stride = this.stride; return pathContext; }, finishPreliminaryPath: function(pathEnding) { this.super.finishPreliminaryPath.apply(this.super, arguments); var path; Module.writeBytes(pathEnding.points, function(points) { path = this.nativeFinishPreliminaryPath(points); }, this); return path; } }); Object.extend(Module.MultiChannelSmoothener.prototype, { smooth: function(pathPart, finish) { this.super.smooth.apply(this.super, arguments); var path; Module.writeBytes(pathPart.points, function(points) { path = Module.PathBuilder.createPath(this.nativeSmooth(points, finish), pathPart.stride); }, this); return path; } }); Object.extend(Module.ParticleBrush.prototype, { configureShape: function(src, callback, context) { if (!this.shapeTexture) { this.shapeTexture = Module.GLTools.createTexture(GLctx.CLAMP_TO_EDGE, GLctx.LINEAR); } Module.GLTools.prepareTexture(this.shapeTexture, src, callback, context); }, configureFill: function(src, callback, context) { if (!this.fillTexture) { this.fillTexture = Module.GLTools.createTexture(GLctx.REPEAT, GLctx.NEAREST); } Module.GLTools.prepareTexture(this.fillTexture, src, function(texture) { this.setFillTextureSize(texture.image.width, texture.image.height); if (callback) callback.call(context || {}); }, this); } }); Object.extend(Module.Intersector.prototype, { setTargetAsStroke: function(path, width) { this.super.setTargetAsStroke.apply(this.super, arguments); Module.writeBytes(path.points, function(points) { this.nativeSetTargetAsStroke(points, path.stride, width); }, this); }, setTargetAsClosedPath: function(path) { this.super.setTargetAsClosedPath.apply(this.super, arguments); Module.writeBytes(path.points, function(points) { this.nativeSetTargetAsClosedPath(points, path.stride); }, this); }, isIntersectingTarget: function(stroke) { this.super.isIntersectingTarget.apply(this.super, arguments); var result; Module.writeBytes(stroke.path.points, function(points) { Module.writeBytes(stroke.path.segmentsBounds(), function(segments) { result = this.nativeIsIntersectingTarget(points, stroke.path.stride, stroke.width, stroke.ts, stroke.tf, stroke.path.bounds, segments); }, this); }, this); return result; }, intersectWithTarget: function(stroke) { this.super.intersectWithTarget.apply(this.super, arguments); var intervals; Module.writeBytes(stroke.path.points, function(points) { Module.writeBytes(stroke.path.segmentsBounds(), function(segments) { intervals = this.nativeIntersectWithTarget(points, stroke.path.stride, stroke.width, stroke.ts, stroke.tf, stroke.path.bounds, segments); }, this); }, this); return intervals; } }); Object.extend(Module.InkEncoder, { encode: function(strokes) { var bytes; var encoder = new Module.InkEncoder(); strokes.forEach(function(stroke) { encoder.encode(stroke); }, this); bytes = Module.readBytes(encoder.getBytes()); encoder.delete(); return bytes; } }); Object.extend(Module.InkEncoder.prototype, { encode: function(stroke, paint) { this.super.encode(stroke); Module.writeBytes(stroke.path.points, function(points) { var ink = { precision: stroke.encodePrecision || 2, path: Module.PathBuilder.createPath(points, stroke.path.stride), width: stroke.width, color: stroke.color, ts: stroke.ts, tf: stroke.tf, randomSeed: Module.getUnsignedInt(stroke.randomSeed), blendMode: stroke.blendMode, paint: Module.getUnsignedInt((paint && !isNaN(paint))?paint:stroke.brush.id), id: Module.getUnsignedInt(stroke.id) }; this.nativeEncode(ink); }, this); } }); Object.extend(Module.InkDecoder, { decode: function(bytes) { var strokes = new Array(); var stroke; var dirtyArea; Module.writeBytes(bytes, function(int64Ptr) { var decoder = new Module.InkDecoder(int64Ptr); while (decoder.hasNext()) { stroke = decoder.decode(); dirtyArea = Module.RectTools.union(dirtyArea, stroke.bounds); strokes.push(stroke); } decoder.delete(); }, this); strokes.bounds = dirtyArea; return strokes; }, getStrokeBrush: function(paint) { throw new Error("Module.InkDecoder.getStrokeBrush(paint, [user]) should be implemented"); } }); Object.extend(Module.InkDecoder.prototype, { decode: function() { this.super.decode.apply(this.super, arguments); var ink = this.nativeDecode(); var brush = Module.InkDecoder.getStrokeBrush(this.paint); var stroke = new Module.Stroke(brush, ink.path, ink.width, ink.color, ink.ts, ink.tf, ink.randomSeed.null?NaN:ink.randomSeed.value, ink.blendMode); this.paint = ink.paint.null?null:ink.paint.value; return stroke; } }); Object.extend(Module.BrushEncoder.prototype, { encode: function(brush) { if (brush instanceof Module.ParticleBrush) { var shapes = new Module.VectorInt64Ptr(); var fills = new Module.VectorInt64Ptr(); this.encodeImages(brush.shapeTexture, shapes); this.encodeImages(brush.fillTexture, fills); this.encodeParticleBrush(brush, shapes, fills, Module.getUnsignedInt(brush.id)); for (var i = 0; i < shapes.size(); i++) Module._free(shapes.get(i).ptr); for (var i = 0; i < fills.size(); i++) Module._free(fills.get(i).ptr); shapes.delete(); fills.delete(); } }, encodeImages: function(textureID, int64Ptrs) { var images = GL.textures[textureID].image?[GL.textures[textureID].image]:GL.textures[textureID].mipmap; if (!images) throw new Error("Texture images not found"); images.forEach(function(image) { // do not encode auto generated images if (image.src.startsWith("data:")) return; var bytes = image.getBytes(); var ptr = Module._malloc(bytes.length); var int64Ptr = {ptr: ptr, length: bytes.length}; Module.HEAPU8.set(bytes, ptr); int64Ptrs.push_back(int64Ptr); }); } }); Object.extend(Module.BrushDecoder.prototype, { decode: function() { this.brushes = new Array(); this.loader = 0; while (this.hasNext()) { var id = this.getBrushID(); var brushID = id.null?null:id.value; var brush = Module.InkDecoder.getStrokeBrush(brushID); if (!brush) { brush = this.getParticleBrush(); brush.id = brushID; this.decodeImages(brush.shapeTexture, this.getShapes()); this.decodeImages(brush.fillTexture, this.getFills()); } this.brushes.push(brush); } if (this.loader == 0) this.onComplete(this.brushes); }, decodeImages: function(textureID, int64Ptrs) { var texture = GL.textures[textureID]; var self = this; this.loader += int64Ptrs.size(); if (int64Ptrs.size() > 1) { var mipmap = new Array(); var cnt = int64Ptrs.size(); for (var i = 0; i < int64Ptrs.size(); i++) { var image = Image.fromBytes(Module.readBytes(int64Ptrs.get(i)), function() { cnt--; self.loader--; if (cnt == 0) { Module.GLTools.completeMipMap(mipmap, function() { texture.mipmap = mipmap; Module.GLTools.initTexture(texture); if (self.loader == 0) self.onComplete(self.brushes); }); } }); mipmap.push(image); } } else { var image = Image.fromBytes(Module.readBytes(int64Ptrs.get(0)), function() { self.loader--; texture.image = this; Module.GLTools.initTexture(texture); if (self.loader == 0) self.onComplete(self.brushes); }); } }, onComplete: function(brushes) {} }); Object.extend(Module.PathOperationEncoder.prototype, { encodeComposeStyle: function(style) { this.super.encodeComposeStyle.apply(this.super, arguments); this.nativeComposeStyle(style.width, style.color, style.blendMode, Module.getUnsignedInt(style.brush.id), Module.getUnsignedInt(style.randomSeed)); }, encodeComposePathPart: function(path, color, variableWidth, variableColor, endStroke) { this.super.encodeComposePathPart.apply(this.super, arguments); if (path.points.length > 0) { Module.writeBytes(path.points, function(points) { this.nativeComposePathPart(points, color, variableWidth, variableColor, endStroke); }, this); } }, encodeAdd: function(strokes) { this.super.encodeAdd.apply(this.super, arguments); var addEncoder = this.nativeAdd(); strokes.forEach(function(stroke) { Module.writeBytes(stroke.path.points, function(points) { var ink = { precision: stroke.encodePrecision || 2, path: Module.PathBuilder.createPath(points, stroke.path.stride), width: stroke.width, color: stroke.color, ts: stroke.ts, tf: stroke.tf, randomSeed: Module.getUnsignedInt(stroke.randomSeed), blendMode: stroke.blendMode, paint: Module.getUnsignedInt(stroke.brush.id), id: Module.getUnsignedInt(stroke.id) }; addEncoder.encodeStroke(ink) }, this); }, this); this.flush(); }, encodeRemove: function(group) { this.super.encodeRemove.apply(this.super, arguments); Module.writeBytes(group, function(group) { this.nativeEncodeRemove(group); }, this); }, encodeUpdateColor: function(group, color) { this.super.encodeUpdateColor.apply(this.super, arguments); Module.writeBytes(group, function(group) { this.nativeEncodeUpdateColor(group, color); }, this); }, encodeUpdateBlendMode: function(group, blendMode) { this.super.encodeUpdateBlendMode.apply(this.super, arguments); Module.writeBytes(group, function(group) { this.nativeEncodeUpdateBlendMode(group, blendMode); }, this); }, encodeSplit: function(splits) { this.super.encodeSplit.apply(this.super, arguments); var splitEncoder = this.nativeSplit(); var affectedArea; splits.forEach(function(split) { var intervals = new Module.VectorInterval(); var intervalIDs = new Array(); affectedArea = Module.RectTools.union(affectedArea, split.bounds); for (var i = 0; i < split.strokes.length; i++) { var strokeID = split.strokes[i].id; var interval = split.intervals[i]; intervals.push(interval); if (strokeID) intervalIDs.push(strokeID); } Module.writeBytes(intervalIDs.toUint32Array(), function(intervalIDs) { splitEncoder.encodeStroke(split.id, affectedArea, intervals, intervalIDs); }, this); intervals.delete(); }, this); this.flush(); }, encodeTransform: function(group, mat) { this.super.encodeTransform.apply(this.super, arguments); Module.writeBytes(group, function(group) { this.nativeEncodeTransform(group, mat); }, this); } }); Object.extend(Module.PathOperationDecoder, { getPathOperationDecoderCallbacksHandler: function(implementation) { var implementationFunctions = ["onComposeStyle", "onComposePathPart", "onComposeAbort", "onAdd", "onRemove", "onUpdateColor", "onUpdateBlendMode", "onSplit", "onTransform"]; implementationFunctions.forEach(function(name) { if (!implementation[name] || typeof implementation[name] != "function") throw new Error("Implementation of \"" + name + "\" missing. Please provide implementation."); }); implementation = Object.clone(implementation); Object.extend(implementation, { composeStyle: function(user, style, paint) { style.brush = Module.InkDecoder.getStrokeBrush(paint, user); style.randomSeed = style.randomSeed.null?NaN:style.randomSeed.value; return style; }, onAddStroke: function(user, strokes, ink) { if (!strokes) strokes = []; var brush = Module.InkDecoder.getStrokeBrush(ink.paint.null?null:ink.paint.value, user); var stroke = new Module.Stroke(brush, ink.path, ink.width, ink.color, ink.ts, ink.tf, ink.randomSeed.null?NaN:ink.randomSeed.value, ink.blendMode); if (!ink.id.null) stroke.id = ink.id.value; strokes.push(stroke); return strokes; }, onSplitStroke: function(splits, id, affectedArea, intervals, intervalIDs) { if (!splits) splits = new Array(); splits.affectedArea = Module.RectTools.union(splits.affectedArea, affectedArea); var split = {id: id, intervals: new Array()}; splits.push(split); intervals.forEach(function(interval, i) { var splitInterval = {fromIndex: interval.fromIndex, toIndex: interval.toIndex, fromTValue: interval.fromTValue, toTValue: interval.toTValue}; if (intervalIDs.length > 0) splitInterval.id = intervalIDs[i]; split.intervals.push(splitInterval); }); return splits; } }); return Module.PathOperationDecoderCallbacksHandlerInterface.implement(implementation); } }); Object.extend(Module.MatTools, { create: function(values) { var mat = this.super.create(); if (values) { for (var name in values) { if (name in mat) mat[name] = values[name]; else console.warn("Key '" + name + "' is not applicable key for the Matrix2D type"); } } return mat; }, makeTranslate: function(tx, ty) { if (arguments.length == 1) { var point = tx; tx = point.x; ty = point.y; } return this.create({tx: tx, ty: ty}); }, makeScale: function(sx, sy) { if (arguments.length == 1) sy = sx; return this.create({a: sx, d: sy}); }, makeRotate: function(alpha) { return this.create({ a: Math.cos(alpha), b: Math.sin(alpha), c: -Math.sin(alpha), d: Math.cos(alpha) }); }, makeScaleAtPoint: function(scale, point) { return this.makeTransformAroundPoint(0, scale, point); }, makeRotationAroundPoint: function(alpha, point) { return this.makeTransformAroundPoint(alpha, 1, point); }, makeTransformAroundPoint: function(alpha, scale, point) { var sx = Math.cos(alpha) * (isNaN(scale)?scale.x:scale); var sy = Math.cos(alpha) * (isNaN(scale)?scale.y:scale); var sin = Math.sin(alpha); return this.create({ a: sx, b: sin, c: -sin, d: sy, tx: point.x - point.x * sx + point.y * sin, ty: point.y - point.x * sin - point.y * sy }); }, fromCSS: function(element) { var result = this.create(); var transform = element.getStyle("transform"); if (transform != "none") { transform = transform.substring(transform.indexOf("(")+1, transform.indexOf(")")).split(/,\s*/g); result.a = parseFloat(transform[0]); result.b = parseFloat(transform[1]); result.c = parseFloat(transform[2]); result.d = parseFloat(transform[3]); result.tx = parseFloat(transform[4]); result.ty = parseFloat(transform[5]); } return result; }, toCSS: function(mat) { return "matrix(" + this.toArray(mat).join(", ") + ")"; }, toArray: function(mat) { return [mat.a, mat.b, mat.c, mat.d, mat.tx, mat.ty]; }, transformRect: function(rect, mat, discardCeil) { return discardCeil?this.super.transformRect(rect, mat):Module.RectTools.ceil(this.super.transformRect(rect, mat)); } }); var enums = [Module.BlendMode, Module.RotationMode, Module.PropertyName, Module.PropertyFunction, Module.InputPhase]; enums.forEach(function(enm) { for (var name in enm) { if (typeof enm[name] == "object") { enm[name]["name"] = name; enm[name]["type"] = enm[name].constructor.name.split("_")[0]; enm[enm[name].value] = enm[name]; } } }); }); Module.getUnsignedInt = Module.getUnsignedLong = function(value) { if (value && !isNaN(value) && value >= 0) return {value: value, null: false}; else return {value: 0, null: true}; return result; } Module.isInt64Ptr = function(int64Ptr) { return int64Ptr && "ptr" in int64Ptr && "length" in int64Ptr && "byteLength" in int64Ptr; } /** * Read bytes from HEAP * * @param {Module.Int64Ptr} int64Ptr * @return {Uint8Array} bytes */ Module.readBytes = function(int64Ptr) { var data = Module.HEAPU8.subarray(int64Ptr.ptr, int64Ptr.ptr+int64Ptr.byteLength); var bytes = new Uint8Array(data, data.byteOffset, int64Ptr.length); return bytes; } /** * Read ints from HEAP * * @param {Module.Int64Ptr} int64Ptr * @return {Uint32Array} ints */ Module.readInts = function(int64Ptr) { var bytes = Module.readBytes(int64Ptr); var ints = new Uint32Array(bytes.buffer); return ints; } /** * Read floats from HEAP * * @param {Module.Int64Ptr} int64Ptr * @return {Float32Array} floats */ Module.readFloats = function(int64Ptr) { var bytes = Module.readBytes(int64Ptr); var floats = new Float32Array(bytes.buffer); return floats; } /** * Handler for extracted data * * @callback WriteBytesCallback * @param {Module.Int64Ptr} int64Ptr * @see Module.writeBytes */ /** * Write bytes to HEAP * * @param {TypedArray} data * @param {WriteBytesCallback} callback * @param {Object} [context={}] callback context */ Module.writeBytes = function(data, callback, context) { if (Module.isInt64Ptr(data)) { callback.call(context || {}, data); return; } else if (!ArrayBuffer.isTypedArray(data)) throw new Error("data is not TypedArray object"); var ptr = Module._malloc(data.byteLength); var int64Ptr = {ptr: ptr, length: data.length, byteLength: data.byteLength}; var bytes = (data instanceof Uint8Array)?data:new Uint8Array(data.buffer, data.byteOffset, data.byteLength); try { Module.HEAPU8.set(bytes, ptr); callback.call(context || {}, int64Ptr); } finally { Module._free(ptr); } } /** * Storage for a bitmap data that can be manipulated or presented on the screen. * It corresponds to an GL texture or a render buffer. * It defines the ration between actual pixels and the abstract layer dimensions. * * @class Module.GenericLayer * @since version 1.3 * @abstract */ Module.GenericLayer = function() { throw new BindingError("GenericLayer has no accessible constructor"); } /** * Layer width * * @memberof Module.GenericLayer.prototype * @member {int} width */ Object.defineProperty(Module.GenericLayer.prototype, "width", { get: function() { return this.layer.width; } }); /** * Layer height * * @memberof Module.GenericLayer.prototype * @member {int} height */ Object.defineProperty(Module.GenericLayer.prototype, "height", { get: function() { return this.layer.height; } }); /** * The bounds of the layer. Equals to (0, 0, width, height). * * @memberof Module.GenericLayer.prototype * @member {Module.Rectangle} bounds */ Object.defineProperty(Module.GenericLayer.prototype, "bounds", { get: function() { return Module.RectTools.create(this.layer.bounds.left, this.layer.bounds.bottom, this.layer.bounds.width, this.layer.bounds.height); } }); Object.extend(Module.GenericLayer.prototype, { /** * Clears layer area with a color * * @method Module.GenericLayer.prototype.clear * @param {(Module.Rectangle | Module.Color)} [rect=bounds] area to clear * @param {Module.Color} [color=Module.Color.TRANSPERENT] */ clear: function(rect, color) { if (!color) { if (Module.Color.is(rect)) { color = rect; rect = null; } else color = Module.Color.TRANSPERENT; } else if (Module.Color.is(rect)) throw new Error("`clear` first argument should be Module.Rectangle"); var wrect = rect ? makeRectFromFlippedRect(rect) : null; this.renderingContext.setTarget(this.layer, wrect); this.renderingContext.clearColor(color); if (wrect) { this.renderingContext.disableTargetClipRect(); } }, /** * Draws points with a brush over the layer * * @method Module.GenericLayer.prototype.draw * @param {Module.Stroke} stroke model instance * @return {Module.Rectangle} affected area */ draw: function(stroke) { if (!Module.Stroke.validatePath(stroke.path)) return null; var drawContext = null; if (stroke.brush instanceof Module.ParticleBrush && !isNaN(stroke.randomSeed)) { drawContext = new Module.StrokeDrawContext(); drawContext.randomSeed = stroke.randomSeed; } var dirtyArea = this.drawStroke(stroke.brush, stroke.path, stroke.width, stroke.color, true, true, stroke.ts, stroke.tf, drawContext); if (drawContext) drawContext.delete(); return dirtyArea; }, /** * Draws points with a brush over the layer * * @method Module.GenericLayer.prototype.drawStroke * @param {Module.StrokeBrush} brush instance of the Module.StrokeBrush class that determines how the stroke is going to be rendered * @param {Module.Path} path points to draw * @param {float} width The width of the stroke. If the stroke has a variable thickness which is included in the control points array, this parameter must be NaN. * @param {Module.Color} color stroke color * If the stroke has a variable alpha which is included in the control points array, alpha component must be NaN. * @param {boolean} roundCapBeginning Cap the stroke with a circle at the start * @param {boolean} roundCapEnding Cap the stroke with a circle at the end * @param {float} ts The starting value for the Catmull-Roll spline parameter * @param {float} tf The final value for the Catmull-Rom spline parameter * @param {Module.StrokeDrawContext} drawContext Module.StrokeDrawContext instance (contains previous path point), applicable only when brush is Module.ParticleBrush * * @return {Module.Rectangle} affected area */ drawStroke: function(brush, path, width, color, roundCapBeggining, roundCapEnding, ts, tf, drawContext) { if (!Module.Stroke.validatePath(path)) return null; var dirtyArea; Module.writeBytes(path.points, function(points) { if (path.transform && !Module.MatTools.isIdentity(path.transform)) { Module.MatTools.transformPath(Module.PathBuilder.createPath(points, path.stride), path.transform); if (!isNaN(width)) width *= Math.sqrt(path.transform.a*path.transform.a + path.transform.c*path.transform.c); } points = Module.readFloats(points); this.renderingContext.setTarget(this.layer); dirtyArea = this.renderingContext.drawStroke(brush, points, path.stride, width, color, roundCapBeggining, roundCapEnding, ts, tf, drawContext, true); if (dirtyArea) dirtyArea = Module.RectTools.create(dirtyArea.left, dirtyArea.bottom, dirtyArea.width, dirtyArea.height); }, this); if (path.transform && !Module.MatTools.isIdentity(path.transform) && Module.isInt64Ptr(path.points)) Module.MatTools.transformPath(path, Module.MatTools.invert(path.transform)); return dirtyArea; }, /** * Fills path with color * * @method Module.GenericLayer.prototype.fillPath * @param {Module.Path} path control points to work with it * @param {Module.Color} color fill color * @param {boolean} antiAliasing is anti aliasing is enabled */ fillPath: function(path, color, antiAliasing) { if (!Module.Stroke.validatePath(path)) return; Module.writeBytes(path.points, function(points) { this.renderingContext.setTarget(this.layer); this.renderingContext.fillPath(points, path.stride, color, !!antiAliasing); }, this); }, /** * Draws the content of the 'source'. The 'source' must use a texture storage. * * @method Module.GenericLayer.prototype.blend * @param {Module.GenericLayer} source layer to be drawn * @param {Module.BlendOptions} options blending configuration */ blend: function(source, options) { if (!options) options = {}; if (!options.mode) options.mode = Module.BlendMode.NORMAL; if (options.rect) { options.sourceRect = options.rect; options.destinationRect = options.rect; } if (options.transform && (options.sourceRect || options.destinationRect)) throw new Error("`sourceRect` and `destinationRect` are not applicable with `transform`"); if (options.sourceRect && !options.destinationRect) throw new Error("With `sourceRect`, `destinationRect` is required"); if (options.destinationRect && !options.sourceRect) throw new Error("With `destinationRect`, `sourceRect`is required"); if (options.transform) { this.renderingContext.setTarget(this.layer); this.renderingContext.drawLayerWithTransform(source.layer, makeMat4FromMatrix2D(options.transform), options.mode); } else if (options.sourceRect && options.destinationRect) { this.renderingContext.setTarget(this.layer); this.renderingContext.drawLayer(source.layer, makeQuadFromFlippedRect(options.sourceRect), makeQuadFromFlippedRect(options.destinationRect), options.mode); } else { this.renderingContext.setTarget(this.layer); this.renderingContext.drawLayerWithTransform(source.layer, null, options.mode); } }, /** * Read pixels data * * @method Module.GenericLayer.prototype.readPixels * @param {Module.Rectangle} [rect=bounds] area to read */ readPixels: function(rect) { this.renderingContext.setTarget(this.layer); return this.renderingContext.readPixels(rect ? makeRectFromFlippedRect(rect) : null); }, /** * Write pixels data * * @method Module.GenericLayer.prototype.writePixels * @param {Uint8Array} bytes pixels data * @param {Module.Rectangle} [rect=bounds] area to write */ writePixels: function(bytes, rect) { if (!bytes) throw new Error("GenericLayer$writePixels 'bytes' parameter is required"); if (!(bytes instanceof Uint8Array)) throw new Error("GenericLayer$writePixels 'bytes' parameter is not instance of Uint8Array"); this.renderingContext.setTarget(this.layer); this.renderingContext.writePixels(rect ? makeRectFromFlippedRect(rect) : null, bytes); } }); /** * Creates ink canvas connected with html canvas. * This layer is the target of screen rendering. * * @class Module.InkCanvas * @extends Module.GenericLayer * @since version 1.0 * @param {HTMLCanvasElement} canvas DOM element related with ink canvas * @param {int} width initial width * @param {int} height initial height * @param {Module.WebGLContextAttributes} [webGLContextAttributes] WebGL context configuration */ Module.InkCanvas = function(canvas, width, height, webGLContextAttributes) { if (!(canvas instanceof HTMLCanvasElement)) throw new Error("canvas is required"); if (!(width > 0 && height > 0)) throw new Error("width and height are required and should be positive whole numbers"); /** * Drawing surface * * @readonly * @memberof Module.InkCanvas.prototype * @member {HTMLCanvasElement} surface */ Object.defineProperty(this, "surface", {value: canvas}); canvas.width = width; canvas.height = height; var renderingContext = new RenderingContext(canvas, webGLContextAttributes || {}); Object.defineProperty(this, "renderingContext", { value: renderingContext }); this.activate(); this.screenHeight = this.screenWidth = Math.max(screen.width, screen.height); var layer = this.renderingContext.createLayer(width, height, 1); layer.init3(null, null, null, false); layer.flipY = true; Object.defineProperty(this, "layer", { get: function() { this.activate(); return layer; } }); /** * Drawing surface context * * @readonly * @memberof Module.InkCanvas.prototype * @member {WebGLRenderingContext} ctx */ Object.defineProperty(this, "ctx", { get: function() { return this.willGLContext.gl; } }); if (webGLContextAttributes && webGLContextAttributes.preserveDrawingBuffer) { Object.defineProperty(this, "frameID", {value: NaN}); this.requestAnimationFrame = function(callback) { throw new Error("This usage is not applicable when preserve drawing buffer"); // callback(Date.now()); }; } else if (Module.ownBackbuffer) { var self = this; this.requestAnimationFrame = function(callback) { self.frameID = requestAnimationFrame(callback); }; this.frameID = -1; } else { var blend = this.super.bind("blend"); Module.InkCanvas.implementBackbuffer(this, blend); } } Module.InkCanvas.extends(Module.GenericLayer); Module.InkCanvas.implementBackbuffer = function(self, blend) { if (arguments.callee.caller != Module.InkCanvas.prototype.constructor) throw new Error("Backbuffer implementation is part from InkCanvas implementation"); var backbuffer = self.createLayer(); var currentFrameUpdates = new Array(); Object.defineProperty(self, "backbuffer", { get: function() { self.activate(); return backbuffer; } }); /** * Adds callback to queue, executed on first animation frame. * This method is not applicable when `preserveDrawingBuffer` WebGL context attribute is setted. * * @method Module.InkCanvas.prototype.requestAnimationFrame * @param {Function} callback A parameter specifying a function to call when it's time to update your animation for the next repaint. * The callback has one single argument, a DOMHighResTimeStamp, * which indicates the current time (the time returned from Performance.now()) for when requestAnimationFrame starts to fire callbacks. * @param {boolean} present is callback affects canvas * @return {int} animation frame ID */ self.requestAnimationFrame = function(callback, present) { if (present) self.present = true; currentFrameUpdates.add(callback); return self.frameID; }; var present = function(timestamp) { while (currentFrameUpdates.length > 0) { var callback = currentFrameUpdates.shift(); callback(timestamp); } if (self.present) { delete self.present; blend(self.backbuffer, {mode: Module.BlendMode.NONE}); } self.frameID = requestAnimationFrame(present); }; self.frameID = requestAnimationFrame(present); Object.extend(Module.InkCanvas.prototype, { clear: function(rect, color) { this.backbuffer.clear(rect, color); this.present = true; }, draw: function(stroke) { this.backbuffer.draw(stroke); this.present = true; }, drawStroke: function(brush, path, width, color, roundCapBeggining, roundCapEnding, ts, tf, drawContext) { var dirtyArea = this.backbuffer.drawStroke(brush, path, width, color, roundCapBeggining, roundCapEnding, ts, tf, drawContext); this.present = true; return dirtyArea; }, fillPath: function(path, color, antiAliasing) { this.backbuffer.fillPath(path, color, antiAliasing); this.present = true; }, blend: function(source, options) { this.backbuffer.blend(source, options); this.present = true; }, readPixels: function(rect) { return this.backbuffer.readPixels(rect); }, writePixels: function(bytes, rect) { this.backbuffer.writePixels(bytes, rect); this.present = true; } }, true); } Object.extend(Module.InkCanvas.prototype, { /** * Creates OffscreenLayer instance * * @method Module.InkCanvas.prototype.createLayer * @param {Module.OffscreenLayerOptions} options layer configuration * @return {Module.OffscreenLayer} layer for offscreen rendering */ createLayer: function(options) { if (!options) options = {}; var layer; if (options.renderbuffer) { if (!options.framebuffer) throw new Error("`framebuffer` is required when `renderbuffer` available"); if (!(options.framebuffer instanceof WebGLFramebuffer)) throw new Error("`framebuffer` is not instance of WebGLFramebuffer"); if (!(options.renderbuffer instanceof WebGLRenderbuffer)) throw new Error("`renderbuffer` is not instance of WebGLRenderbuffer"); layer = this.renderingContext.createLayerFromGLBuffers(options.framebuffer, options.renderbuffer, !!options.ownGlResources); } else if (options.framebuffer) { if (!(options.framebuffer instanceof WebGLFramebuffer)) throw new Error("`framebuffer` is not instance of WebGLFramebuffer"); layer = this.renderingContext.createLayerFromGLFramebuffer(options.framebuffer, !!options.ownGlResources); } else if (options.texture) { if (!(options.texture instanceof WebGLTexture)) throw new Error("`texture` is not instance of WebGLTexture"); if (options.width > 0 && options.height > 0) { layer = this.renderingContext.createLayer(options.width, options.height, 1); layer.init3(null, null, options.texture, !!options.ownGlResources); } else if (options.texture.image) { layer = this.renderingContext.createLayer(options.texture.image.width, options.texture.image.height, 1); layer.init3(null, null, options.texture, !!options.ownGlResources); } else throw new Error("`width` and `height` are required when `texture` is available"); } else if (options.width > 0 && options.height > 0) { if (options.scaleFactor > 0) { layer = this.renderingContext.createLayer(options.width, options.height, options.scaleFactor); layer.init1(true); } else { layer = this.renderingContext.createLayer(options.width, options.height, 1); layer.init1(true); } } else { layer = this.renderingContext.createLayer(this.screenWidth, this.screenHeight, 1); layer.init1(true); } var OffscreenLayer = Function.create("OffscreenLayer", function(layer, renderingContext) { this.layer = layer; this.renderingContext = renderingContext; // init_ClassHandle in WacomInkEngine.js // this.isAliasOf = function(other) {return this.nativeLayer.isAliasOf(other.nativeLayer);}; // this.clone = function() {return this.nativeLayer.clone();}; this.delete = function() { layer.delete(); }; this.isDeleted = function() { return layer.isDeleted(); }; this.deleteLater = function() { layer.deleteLater(); return this; }; }); OffscreenLayer.extends(Module.GenericLayer); Object.defineProperty(OffscreenLayer.prototype, "renderbuffer", { get: function() { return this.layer.renderbuffer; } }); Object.defineProperty(OffscreenLayer.prototype, "framebuffer", { get: function() { return this.layer.framebuffer; } }); Object.defineProperty(OffscreenLayer.prototype, "texture", { get: function() { return this.layer.colorTexture; } }); return new OffscreenLayer(layer, this.renderingContext); }, /** * Resize canvas * * @method Module.InkCanvas.prototype.resize * @param {int} width * @param {int} height */ resize: function(width, height) { this.surface.width = width; this.surface.height = height; this.layer.resize(width, height); }, activate: function() { Module.canvas = this.surface; window.GLctx = this.renderingContext.willGLContext.gl; }, delete: function() { cancelAnimationFrame(this.frameID); this.layer.delete(); this.backbuffer.delete(); }, deleteLater: function() { this.layer.deleteLater(); this.backbuffer.deleteLater(); return this; }, isDeleted: function() { return this.layer.isDeleted(); } }, true); /** * Stroke model * * @class Module.Stroke * @since version 1.4 * * @param {Module.StrokeBrush} brush * @param {Module.Path} path * @param {float} width * @param {Module.Color} color * @param {float} ts * @param {float} tf * @param {int} [randomSeed=NaN] * @param {Module.BlendMode} [blendMode=Module.BlendMode.NORMAL] */ Module.Stroke = function(brush, path, width, color, ts, tf, randomSeed, blendMode) { this.brush = brush; this.brush.mutable = true; this.path = path; this.width = width; this.color = Object.clone(color); this.ts = ts; this.tf = tf; this.randomSeed = (randomSeed && !isNaN(randomSeed))?randomSeed:NaN; this.blendMode = blendMode || Module.BlendMode.NORMAL; this.initPath(); } /** * Stroke length * * @memberof Module.Stroke.prototype * @member {int} length */ Object.defineProperty(Module.Stroke.prototype, "length", {enumerable: true, get: function() {return this.path.length;}}); /** * Stroke bounds * * @memberof Module.Stroke.prototype * @member {Module.Rectangle} bounds */ Object.defineProperty(Module.Stroke.prototype, "bounds", {enumerable: true, get: function() {return this.path.bounds;}}); Object.defineProperty(Module.Stroke.prototype, "data", {enumerable: true, get: function() {return {path: {points: this.path.points, stride: this.path.stride}, width: this.width, color: this.color, ts: this.ts, tf: this.tf, randomSeed: this.randomSeed, blendMode: this.blendMode};}}); Object.extend(Module.Stroke, { /** * Creates stroke point object * * @method Module.Stroke.createPoint * @param {int} [x=Infinity] * @param {int} [y=Infinity] * @param {float} [width=Infinity] * @param {float} [alpha=Infinity] * @return {Module.Point2D} new point, with args as properties */ createPoint: function(x, y, width, alpha) { return { x: (typeof x == "undefined" || x == null || isNaN(x))?Infinity:x, y: (typeof y == "undefined" || y == null || isNaN(y))?Infinity:y, width: (typeof width == "undefined" || width == null || isNaN(width))?Infinity:width, alpha: (typeof alpha == "undefined" || alpha == null || isNaN(alpha))?Infinity:alpha }; }, fromJSON: function(brush, data) { if (!data.width) data.width = NaN; if (!data.color.alpha) data.color.alpha = NaN; if (!data.path) data.path = Module.PathBuilder.createPath(data.points, data.stride); return new Module.Stroke(brush, data.path, data.width, data.color, data.ts, data.tf, data.randomSeed, Module.BlendMode[data.blendMode]); }, validatePath: function(path) { if (!path) return false; if (Module.isInt64Ptr(path.points)) return true; if (path.points.length < 4 * path.stride) { if (path.points.length > 0) Module.printErr("WARNING: Less than needed minimum of points passed (At least 4 points are needed to define a path)!"); return false; } if (path.points.length % path.stride != 0) { Module.printErr("WARNING: The points array size (" + path.points.length + ") is should be a multiple of the stride property (" + path.stride + ")!"); return false; } return true; }, normalizeStrokeData: function(data) { if (!data.path) throw new Error("StrokeData path is required"); if (!("width" in data)) data.width = NaN; if (!("ts" in data)) data.ts = 0; if (!("tf" in data)) data.tf = 1; if (!("randomSeed" in data)) data.randomSeed = NaN; if (!("blendMode" in data)) data.blendMode = Module.BlendMode.NORMAL; } }); Object.extend(Module.Stroke.prototype, { initPath: function() { Object.defineProperty(this.path, "length", {enumerable: true, get: function() {return this.points.length / this.stride;}}); Object.extend(this.path, { get: function(i) { var base = i * this.stride; var point = {x: this.points[base], y: this.points[base + 1]}; if (this.index.width > -1) point.width = this.points[base + this.index.width]; if (this.index.alpha > -1) point.alpha = this.points[base + this.index.alpha]; return point; }, set: function(i, point) { var base = i * this.stride; this.points[base] = point.x; this.points[base + 1] = point.y; if (this.index.width > -1) this.points[base + this.index.width] = point.width; if (this.index.alpha > -1) this.points[base + this.index.alpha] = point.alpha; }, getPart: function(fromIndex, toIndex) { return { points: new Float32Array(this.points.subarray(fromIndex * this.stride, (toIndex+1) * this.stride)), stride: this.stride, index: this.index }; }, calculateBounds: function(width, scattering) { this.bounds = null; this.segments = new Array(this.length - 3); Module.writeBytes(this.points, function(points) { for (var i = 0; i < this.segments.length; i++) { var segment = Module.calculateSegmentBounds(points, this.stride, width, i, scattering || 0); this.segments[i] = segment; this.bounds = Module.RectTools.union(this.bounds, segment); } }, this); }, transformBounds: function(mat) { // for (var i = 0; i < this.segments.length; i++) // this.segments[i] = Module.MatTools.transformRect(this.segments[i], mat); this.bounds = Module.MatTools.transformRect(this.bounds, mat); }, segmentsBounds: function() { var segments = new Float32Array(this.segments.length * 4); this.segments.forEach(function(segment, i) { segments[i * 4] = segment.left; segments[i * 4 + 1] = segment.top; segments[i * 4 + 2] = segment.width; segments[i * 4 + 3] = segment.height; }); return segments; } }); var points = this.path.points; if (Module.isInt64Ptr(points)) { this.path.calculateBounds(this.width, this.brush.scattering); this.path.points = Module.readFloats(points); } else { if (points instanceof Array) this.path.points = points.toFloat32Array(); else if (!(points instanceof Float32Array)) throw new Error("Invalid path points type"); else if (points.byteOffset > 0) this.path.points = new Float32Array(points); this.path.calculateBounds(this.width, this.brush.scattering); } if (!this.path.index) { this.path.index = new Object(); switch (this.path.stride) { case 2: this.path.index.width = -1; this.path.index.alpha = -1; break; case 3: this.path.index.width = isNaN(this.width)?2:-1;; this.path.index.alpha = isNaN(this.width)?-1:2;; break; case 4: this.path.index.width = 2; this.path.index.alpha = 3; break; default: throw new Error("Invalid stride: " + this.path.stride); } } }, /** * @method Module.Stroke.prototype.getPoint * @param {int} idx * @return {Module.Point2D} point on idx */ getPoint: function(idx) { return this.path.get(idx); }, /** * Sets point stroke * * @method Module.Stroke.prototype.setPoint * @param {int} idx * @param {Module.Point2D} point stroke point */ setPoint: function(idx, point) { this.path.set(idx, point); }, /** * When stroke path is modified, bounds should be updated * * @method Module.Stroke.prototype.updateBounds */ updateBounds: function() { this.path.calculateBounds(this.width, this.brush.scattering); }, /** * Split stroke * * @method Module.Stroke.prototype.split * @param {Module.VectorInterval} intervals * @param {Module.IntersectorTargetType} type * @return {Module.Split} intersection result */ split: function(intervals, type) { var result; var dirtyArea; var intersect = false; var strokes = new Array(); var strokesIntervals = new Array(); var holes = new Array(); var selected = new Array(); for (var i = 0; i < intervals.size(); i++) { var interval = intervals.get(i); var subStroke = this.subStroke(interval.fromIndex, interval.toIndex, interval.fromTValue, interval.toTValue); if (interval.inside) { intersect = true; dirtyArea = Module.RectTools.union(dirtyArea, subStroke.bounds); } if (type == Module.IntersectorTargetType.STROKE) { if (interval.inside) holes.push(interval); else { strokesIntervals.push(interval); strokes.push(subStroke); } } else { if (interval.inside) selected.push(subStroke); strokesIntervals.push(interval); strokes.push(subStroke); } } result = {intersect: intersect}; if (intersect) { result.stroke = this; result.bounds = dirtyArea; result.strokes = strokes; result.intervals = strokesIntervals; if (type == Module.IntersectorTargetType.STROKE) result.holes = holes; else result.selected = selected; } return result; }, /** * Creates new stroke based on current * * @method Module.Stroke.prototype.subStroke * @param {int} fromIndex start point idx * @param {int} toIndex end point idx * @param {float} fromTValue * @param {float} toTValue * @return {Module.Stroke} new stroke */ subStroke: function(fromIndex, toIndex, fromTValue, toTValue) { if (fromIndex == 0 && toIndex == this.length - 1 && fromTValue == this.ts && toTValue == this.tf) return this; var path = this.path.getPart(fromIndex, toIndex); var stroke = new Module.Stroke(this.brush, path, this.width, this.color, fromTValue, toTValue, this.randomSeed, this.blendMode); return stroke; }, /** * Transform stroke * * @method Module.Stroke.prototype.transform * @param {Module.Matrix2D} mat transform matrix */ transform: function(mat) { Module.writeBytes(this.path.points, function(points) { var path = Module.PathBuilder.createPath(points, this.path.stride); Module.MatTools.transformPath(path, mat); this.path.points = Module.readFloats(points); }, this); this.path.calculateBounds(this.width, this.brush.scattering); }, toJSON: function() { var stroke = { path: { points: this.path.points.toArray(), stride: this.path.stride }, width: this.width, color: this.color, ts: this.ts, tf: this.tf, randomSeed: this.randomSeed, blendMode: this.blendMode.value }; return stroke; } }); /** * Stroke painter * * @class Module.StrokeRenderer * @since version 1.3 * @param {Module.InkCanvas} canvas view layer * @param {(Module.GenericLayer | Module.OffscreenLayerOptions)} [layerORoptions] * If argument is layer, it is a buffer layer for stroke building. In this case preliminary curve draw is over canvas. * When not available is auto created with default size (device screen bounds) or user defined size. */ Module.StrokeRenderer = function(canvas, layerORoptions) { this.canvas = canvas; var layer = (layerORoptions instanceof Module.GenericLayer)?layerORoptions:null; var options = (layerORoptions && !layer)?layerORoptions:new Object(); this.layer = layer || canvas.createLayer(options); this.layerOptions = options; this.ownLayer = !layer; this.restart = true; } Object.extend(Module.StrokeRenderer.prototype, { /** * Defines rendering brush * * @memberof Module.StrokeRenderer * @member {Module.StrokeBrush} brush */ brush: null, /** * Defines rendering color * * @memberof Module.StrokeRenderer * @member {Module.Color} color */ color: null, /** * Defines stroke width. Default value is NaN. * NaN value instructs 'Module.StrokeRenderer' that the path has a variable width * which will be provided by the control points array. * * @memberof Module.StrokeRenderer * @member {float} width */ width: NaN, /** * Random generator seed, applicable only when brush is Module.ParticleBrush * * @memberof Module.StrokeRenderer * @member {int} randomSeed */ randomSeed: NaN, /** * Used when blending updated area and stroke. Default is NORMAL. * * @memberof Module.StrokeRenderer * @member {Module.BlendMode} blendMode */ blendMode: null, /** * Buffer layer for stroke building with preliminary curve, default is null. * Auto created when draw preliminary path. * * @memberof Module.StrokeRenderer * @member {Module.Layer} preliminaryLayer */ preliminaryLayer: null, /** * Current stroke area * * @memberof Module.StrokeRenderer * @member {Module.Rectangle} strokeBounds */ strokeBounds: null, /** * Current modified segments area * * @memberof Module.StrokeRenderer * @member {Module.Rectangle} updatedArea