From 03d1d3c726309083e15506468ab098795507d0b9 Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Mon, 22 Jul 2024 01:02:48 +0100 Subject: [PATCH 01/12] background images v1 --- netbox_floorplan/admin.py | 9 + netbox_floorplan/api/serializers.py | 14 +- netbox_floorplan/api/urls.py | 2 +- netbox_floorplan/api/views.py | 6 +- netbox_floorplan/forms.py | 19 +- .../migrations/0008_floorplanimage.py | 30 + .../migrations/0009_floorplanimage_file.py | 19 + .../0010_floorplanimage_external_url.py | 18 + .../0011_floorplanimage_comments.py | 18 + .../0012_alter_floorplan_options_and_more.py | 27 + netbox_floorplan/models.py | 100 +- netbox_floorplan/navigation.py | 5 +- .../static/netbox_floorplan/floorplan/edit.js | 66 +- .../netbox_floorplan/floorplan/utils.js | 34 +- .../vendors/fabric-js-6.0.2.js | 1 + .../static/netbox_floorplan/vendors/fabric.js | 31187 ---------------- .../static/netbox_floorplan/vendors/jq.js | 2 - .../netbox_floorplan/vendors/jquery-3.7.1.js | 2 + netbox_floorplan/tables.py | 21 +- .../netbox_floorplan/floorplan_edit.html | 38 +- .../netbox_floorplan/floorplan_view.html | 7 +- .../netbox_floorplan/floorplanimage.html | 49 + .../netbox_floorplan/floorplanimage_edit.html | 45 + netbox_floorplan/urls.py | 37 +- netbox_floorplan/utils.py | 5 +- netbox_floorplan/views.py | 21 + 26 files changed, 551 insertions(+), 31231 deletions(-) create mode 100644 netbox_floorplan/migrations/0008_floorplanimage.py create mode 100644 netbox_floorplan/migrations/0009_floorplanimage_file.py create mode 100644 netbox_floorplan/migrations/0010_floorplanimage_external_url.py create mode 100644 netbox_floorplan/migrations/0011_floorplanimage_comments.py create mode 100644 netbox_floorplan/migrations/0012_alter_floorplan_options_and_more.py create mode 100644 netbox_floorplan/static/netbox_floorplan/vendors/fabric-js-6.0.2.js delete mode 100644 netbox_floorplan/static/netbox_floorplan/vendors/fabric.js delete mode 100644 netbox_floorplan/static/netbox_floorplan/vendors/jq.js create mode 100644 netbox_floorplan/static/netbox_floorplan/vendors/jquery-3.7.1.js create mode 100644 netbox_floorplan/templates/netbox_floorplan/floorplanimage.html create mode 100644 netbox_floorplan/templates/netbox_floorplan/floorplanimage_edit.html diff --git a/netbox_floorplan/admin.py b/netbox_floorplan/admin.py index e69de29..88c45d7 100644 --- a/netbox_floorplan/admin.py +++ b/netbox_floorplan/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from .models import Floorplan + + +@admin.register(Floorplan) +class FloorplanAdmin(admin.ModelAdmin): + list_display = ( + "pk", + ) diff --git a/netbox_floorplan/api/serializers.py b/netbox_floorplan/api/serializers.py index 3b3afdf..d38bd8c 100644 --- a/netbox_floorplan/api/serializers.py +++ b/netbox_floorplan/api/serializers.py @@ -1,14 +1,24 @@ from rest_framework import serializers from netbox.api.serializers import NetBoxModelSerializer -from ..models import Floorplan +from ..models import Floorplan, FloorplanImage +class FloorplanImageSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='plugins-api:netbox_floorplan-api:floorplanimage-detail') + + class Meta: + model = FloorplanImage + fields = ['id', 'url', 'name', 'file', 'external_url', 'filename', 'comments', 'tags', 'custom_fields', 'created', 'last_updated'] + brief_fields = ['id', 'url', 'name', 'file', 'filename', 'external_url'] + class FloorplanSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField( view_name='plugins-api:netbox_floorplan-api:floorplan-detail') + assigned_image = FloorplanImageSerializer(nested=True, required=False, allow_null=True) class Meta: model = Floorplan - fields = ['id', 'url', 'site', 'location', 'background_image', + fields = ['id', 'url', 'site', 'location', 'assigned_image', 'width', 'height', 'tags', 'custom_fields', 'created', 'last_updated', 'canvas', 'measurement_unit'] diff --git a/netbox_floorplan/api/urls.py b/netbox_floorplan/api/urls.py index 8ea1743..7429a91 100644 --- a/netbox_floorplan/api/urls.py +++ b/netbox_floorplan/api/urls.py @@ -5,5 +5,5 @@ router = NetBoxRouter() router.register('floorplans', views.FloorplanViewSet) - +router.register('floorplanimages', views.FloorplanImageViewSet) urlpatterns = router.urls diff --git a/netbox_floorplan/api/views.py b/netbox_floorplan/api/views.py index 543522c..7c0879e 100644 --- a/netbox_floorplan/api/views.py +++ b/netbox_floorplan/api/views.py @@ -1,10 +1,14 @@ from netbox.api.viewsets import NetBoxModelViewSet from .. import filtersets, models -from .serializers import FloorplanSerializer +from .serializers import FloorplanSerializer, FloorplanImageSerializer class FloorplanViewSet(NetBoxModelViewSet): queryset = models.Floorplan.objects.all() serializer_class = FloorplanSerializer filterset_class = filtersets.FloorplanFilterSet + +class FloorplanImageViewSet(NetBoxModelViewSet): + queryset = models.FloorplanImage.objects.prefetch_related('tags') + serializer_class = FloorplanImageSerializer diff --git a/netbox_floorplan/forms.py b/netbox_floorplan/forms.py index 132fda4..323f176 100644 --- a/netbox_floorplan/forms.py +++ b/netbox_floorplan/forms.py @@ -1,12 +1,27 @@ from netbox.forms import NetBoxModelForm -from .models import Floorplan +from .models import Floorplan, FloorplanImage from dcim.models import Rack, Device +from utilities.forms.rendering import FieldSet +from utilities.forms.fields import CommentField +class FloorplanImageForm(NetBoxModelForm): + comments = CommentField() + fieldsets = ( + FieldSet(('name', 'file', 'external_url', 'comments'), name='General'), + FieldSet(('comments', 'tags'), name=''), + ) + class Meta: + model = FloorplanImage + fields = [ + 'name', + 'file', + 'external_url' + ] class FloorplanForm(NetBoxModelForm): class Meta: model = Floorplan - fields = ['site', 'location', 'background_image', 'width', 'height'] + fields = ['site', 'location', 'assigned_image', 'width', 'height'] class FloorplanRackFilterForm(NetBoxModelForm): diff --git a/netbox_floorplan/migrations/0008_floorplanimage.py b/netbox_floorplan/migrations/0008_floorplanimage.py new file mode 100644 index 0000000..a643ad8 --- /dev/null +++ b/netbox_floorplan/migrations/0008_floorplanimage.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.7 on 2024-07-19 23:49 + +import taggit.managers +import utilities.json +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0115_convert_dashboard_widgets'), + ('netbox_floorplan', '0007_alter_floorplan_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='FloorplanImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('name', models.CharField(max_length=128)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ('name',), + }, + ), + ] diff --git a/netbox_floorplan/migrations/0009_floorplanimage_file.py b/netbox_floorplan/migrations/0009_floorplanimage_file.py new file mode 100644 index 0000000..f09a4af --- /dev/null +++ b/netbox_floorplan/migrations/0009_floorplanimage_file.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.7 on 2024-07-20 19:02 + +import netbox_floorplan.utils +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_floorplan', '0008_floorplanimage'), + ] + + operations = [ + migrations.AddField( + model_name='floorplanimage', + name='file', + field=models.FileField(blank=True, upload_to=netbox_floorplan.utils.file_upload), + ), + ] diff --git a/netbox_floorplan/migrations/0010_floorplanimage_external_url.py b/netbox_floorplan/migrations/0010_floorplanimage_external_url.py new file mode 100644 index 0000000..9f4d7f4 --- /dev/null +++ b/netbox_floorplan/migrations/0010_floorplanimage_external_url.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2024-07-20 19:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_floorplan', '0009_floorplanimage_file'), + ] + + operations = [ + migrations.AddField( + model_name='floorplanimage', + name='external_url', + field=models.URLField(blank=True, max_length=255), + ), + ] diff --git a/netbox_floorplan/migrations/0011_floorplanimage_comments.py b/netbox_floorplan/migrations/0011_floorplanimage_comments.py new file mode 100644 index 0000000..3f97929 --- /dev/null +++ b/netbox_floorplan/migrations/0011_floorplanimage_comments.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2024-07-20 20:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_floorplan', '0010_floorplanimage_external_url'), + ] + + operations = [ + migrations.AddField( + model_name='floorplanimage', + name='comments', + field=models.TextField(blank=True), + ), + ] diff --git a/netbox_floorplan/migrations/0012_alter_floorplan_options_and_more.py b/netbox_floorplan/migrations/0012_alter_floorplan_options_and_more.py new file mode 100644 index 0000000..bc57951 --- /dev/null +++ b/netbox_floorplan/migrations/0012_alter_floorplan_options_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.7 on 2024-07-20 20:53 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_floorplan', '0011_floorplanimage_comments'), + ] + + operations = [ + migrations.AlterModelOptions( + name='floorplan', + options={'ordering': ('site', 'location', 'assigned_image', 'width', 'height', 'measurement_unit')}, + ), + migrations.RemoveField( + model_name='floorplan', + name='background_image', + ), + migrations.AddField( + model_name='floorplan', + name='assigned_image', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='netbox_floorplan.floorplanimage'), + ), + ] diff --git a/netbox_floorplan/models.py b/netbox_floorplan/models.py index 3f8ac7a..5a22722 100644 --- a/netbox_floorplan/models.py +++ b/netbox_floorplan/models.py @@ -1,13 +1,98 @@ +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse - from netbox.models import NetBoxModel +from dcim.models import Rack, Device from .utils import file_upload -from dcim.models import Rack, Device + +class FloorplanImage(NetBoxModel): + """ + A Floorplan Image is effectively a background image + """ + name = models.CharField( + help_text='Can be used to quickly identify a particular image', + max_length=128, + blank=False, + null=False + ) + + file = models.FileField( + upload_to=file_upload, + blank=True + ) + + external_url = models.URLField( + blank=True, + max_length=255 + ) + + comments = models.TextField( + blank=True + ) + + def get_absolute_url(self): + return reverse('plugins:netbox_floorplan:floorplanimage', args=[self.pk]) + + def __str__(self): + return f'{self.name}' + + class Meta: + ordering = ('name',) + + @property + def size(self): + """ + Wrapper around `document.size` to suppress an OSError in case the file is inaccessible. Also opportunistically + catch other exceptions that we know other storage back-ends to throw. + """ + expected_exceptions = [OSError] + + try: + from botocore.exceptions import ClientError + expected_exceptions.append(ClientError) + except ImportError: + pass + + try: + return self.file.size + except: + return None + + @property + def filename(self): + filename = self.file.name.rsplit('/', 1)[-1] + return filename + + def clean(self): + super().clean() + + # Must have an uploaded document or an external URL. cannot have both + if not self.file and self.external_url == '': + raise ValidationError("A document must contain an uploaded file or an external URL.") + if self.file and self.external_url: + raise ValidationError("A document cannot contain both an uploaded file and an external URL.") + + def delete(self, *args, **kwargs): + + # Check if its a document or a URL + if self.external_url == '': + + _name = self.file.name + + # Delete file from disk + super().delete(*args, **kwargs) + self.file.delete(save=False) + + # Restore the name of the document as it's re-used in the notifications later + self.file.name = _name + else: + # Straight delete of external URL + super().delete(*args, **kwargs) class Floorplan(NetBoxModel): + site = models.ForeignKey( to='dcim.Site', blank=True, @@ -20,10 +105,12 @@ class Floorplan(NetBoxModel): null=True, on_delete=models.PROTECT ) - background_image = models.ImageField( - upload_to=file_upload, + + assigned_image = models.ForeignKey( + to='FloorplanImage', blank=True, - null=True + null=True, + on_delete=models.SET_NULL ) width = models.DecimalField( max_digits=10, @@ -50,7 +137,7 @@ class Floorplan(NetBoxModel): canvas = models.JSONField(default=dict) class Meta: - ordering = ('site', 'location', 'background_image', + ordering = ('site', 'location', 'assigned_image', 'width', 'height', 'measurement_unit') def __str__(self): @@ -83,6 +170,7 @@ def mapped_racks(self): int(subobj["custom_meta"]["object_id"])) return drawn_racks + @property def mapped_devices(self): drawn_devices = [] diff --git a/netbox_floorplan/navigation.py b/netbox_floorplan/navigation.py index d2f539f..e855cc6 100644 --- a/netbox_floorplan/navigation.py +++ b/netbox_floorplan/navigation.py @@ -10,9 +10,10 @@ # menu_buttons = ( PluginMenuItem( - link="plugins:netbox_floorplan:floorplan_list", - link_text="Floorplans", + link="plugins:netbox_floorplan:floorplanimage_list", + link_text="Floorplan Images", ), + ) diff --git a/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js b/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js index 8bb8de5..5ae42f1 100644 --- a/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js +++ b/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js @@ -15,6 +15,7 @@ import { init_floor_plan } from "/static/netbox_floorplan/floorplan/utils.js"; + var csrf = document.getElementById('csrf').value; var obj_pk = document.getElementById('obj_pk').value; var obj_name = document.getElementById('obj_name').value; @@ -25,6 +26,7 @@ var location_id = document.getElementById('location_id').value; htmx.ajax('GET', `/plugins/floorplan/floorplans/racks/?floorplan_id=${obj_pk}`, { target: '#rack-card', swap: 'innerHTML', trigger: 'load' }) htmx.ajax('GET', `/plugins/floorplan/floorplans/devices/?floorplan_id=${obj_pk}`, { target: '#unrack-card', swap: 'innerHTML', trigger: 'load' }) + fabric.Object.prototype.set({ snapThreshold: 45, snapAngle: 45 @@ -102,7 +104,8 @@ function add_wall() { left: 0, width: 10, height: 500, - fill: '#6ea8fe', + //fill: '#6ea8fe', + fill: 'red', opacity: 0.8, lockRotation: false, originX: "center", @@ -119,7 +122,6 @@ function add_wall() { "object_type": "wall", }, }); - var group = new fabric.Group([wall]); group.setControlsVisibility({ @@ -163,7 +165,7 @@ function add_area() { }); var group = new fabric.Group([wall]); - group.setControlsVisibility({ + group.setControlsVisibility({ mt: true, mb: true, ml: true, @@ -175,6 +177,7 @@ function add_area() { }) canvas.add(group); canvas.centerObject(group); + canvas.requestRenderAll(); } window.add_area = add_area; @@ -251,6 +254,11 @@ window.send_back = send_back; function set_dimensions() { $('#control_unit_modal').modal('show'); } +function set_background() { + $('#background_unit_modal').modal('show'); +} + +window.set_background = set_background; window.set_dimensions = set_dimensions; function add_text() { @@ -413,6 +421,7 @@ window.set_color = set_color; function set_zoom(new_current_zoom) { current_zoom = new_current_zoom; canvas.setZoom(current_zoom); + canvas.requestRenderAll() document.getElementById("zoom").value = current_zoom; } window.set_zoom = set_zoom; @@ -442,6 +451,57 @@ window.center_pan_on_slected_object = center_pan_on_slected_object; // start set scale ----------------------------------------------------------------------------- ! +function update_background() { + var assigned_image = document.getElementById("id_assigned_image").value; + if (assigned_image == "") { assigned_image = null; } + var floor_json = canvas.toJSON(["id", "text", "_controlsVisibility", "custom_meta", "lockMovementY", "lockMovementX", "evented", "selectable"]); + + $.ajax({ + type: "PATCH", + url: `/api/plugins/floorplan/floorplans/${obj_pk}/`, + dataType: "json", + headers: { + "X-CSRFToken": csrf, + "Content-Type": "application/json" + }, + data: JSON.stringify({ + "assigned_image": assigned_image, + "canvas": floor_json + }), + error: function (err) { + console.log(`Error: ${err}`); + } + }).done(function (floorplan) { + if (floorplan.assigned_image != null) { + var img_url = ""; + if (floorplan.assigned_image.external_url != "") { + img_url = floorplan.assigned_image.external_url; + } else { + img_url = floorplan.assigned_image.file; + } + + var img = fabric.Image.fromURL(img_url, function(img) { + let scaleRatio = Math.max(canvas.width / img.width, canvas.height / img.height); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { + scaleX: scaleRatio, + scaleY: scaleRatio, + left: canvas.width / 2, + top: canvas.height / 2, + originX: 'middle', + originY: 'middle' + }); + }); + + } else { + canvas.setBackgroundImage().renderAll(); + } + canvas.renderAll(); + $('#background_unit_modal').modal('hide'); + }); +} + +window.update_background = update_background; + function update_dimensions() { var width = document.getElementById("width_value").value; diff --git a/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js b/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js index b1632b0..e61116e 100644 --- a/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js +++ b/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js @@ -12,6 +12,7 @@ export { init_floor_plan }; + function resize_canvas(canvas, window) { var bob_width = $("#content-container").width(); var window_width = $(window).width(); @@ -21,6 +22,8 @@ function resize_canvas(canvas, window) { var canvas_height = window_height - 100; canvas.setWidth(canvas_width); canvas.setHeight(canvas_height); +// canvas.backgroundImage.scaleToWidth(canvas_width); +// canvas.backgroundImage.scaleToHeight(canvas_height); canvas.renderAll(); } @@ -132,20 +135,49 @@ function move_pan(opt, canvas) { } } + + + function init_floor_plan(floorplan_id, canvas, mode) { if (floorplan_id === undefined || floorplan_id === null || floorplan_id === "") { return; } + var target_image = 0; const floorplan_call = $.get(`/api/plugins/floorplan/floorplans/?id=${floorplan_id}`); floorplan_call.done(function (floorplan) { floorplan.results.forEach((floorplan) => { + target_image = floorplan.assigned_image canvas.loadFromJSON(JSON.stringify(floorplan.canvas), canvas.renderAll.bind(canvas), function (o, object) { if (mode == "readonly") { object.set('selectable', false); } - + if (floorplan.assigned_image != null) { + var img_url = ""; + if (floorplan.assigned_image.external_url != "") { + img_url = floorplan.assigned_image.external_url; + } else { + img_url = floorplan.assigned_image.file; + } + + var img = fabric.Image.fromURL(img_url, function(img) { + let scaleRatio = Math.max(canvas.width / img.width, canvas.height / img.height); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { + scaleX: scaleRatio, + scaleY: scaleRatio, + left: canvas.width / 2, + top: canvas.height / 2, + originX: 'middle', + originY: 'middle' + }); + }); + + + } else { + canvas.setBackgroundImage().renderAll(); + } + canvas.renderAll(); }); }); reset_zoom(canvas); diff --git a/netbox_floorplan/static/netbox_floorplan/vendors/fabric-js-6.0.2.js b/netbox_floorplan/static/netbox_floorplan/vendors/fabric-js-6.0.2.js new file mode 100644 index 0000000..38f1c3d --- /dev/null +++ b/netbox_floorplan/static/netbox_floorplan/vendors/fabric-js-6.0.2.js @@ -0,0 +1 @@ +var fabric=fabric||{version:"5.2.1"};if("undefined"!=typeof exports?exports.fabric=fabric:"function"==typeof define&&define.amd&&define([],function(){return fabric}),"undefined"!=typeof document&&"undefined"!=typeof window)document instanceof("undefined"!=typeof HTMLDocument?HTMLDocument:Document)?fabric.document=document:fabric.document=document.implementation.createHTMLDocument(""),fabric.window=window;else{var jsdom=require("jsdom"),virtualWindow=new jsdom.JSDOM(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"),{features:{FetchExternalResources:["img"]},resources:"usable"}).window;fabric.document=virtualWindow.document,fabric.jsdomImplForWrapper=require("jsdom/lib/jsdom/living/generated/utils").implForWrapper,fabric.nodeCanvas=require("jsdom/lib/jsdom/utils").Canvas,fabric.window=virtualWindow,DOMParser=fabric.window.DOMParser}function resizeCanvasIfNeeded(t){var e=t.targetCanvas,i=e.width,r=e.height,n=t.destinationWidth,s=t.destinationHeight;i===n&&r===s||(e.width=n,e.height=s)}function copyGLTo2DDrawImage(t,e){var i=t.canvas,r=e.targetCanvas,n=r.getContext("2d");n.translate(0,r.height),n.scale(1,-1);var s=i.height-r.height;n.drawImage(i,0,s,r.width,r.height,0,0,r.width,r.height)}function copyGLTo2DPutImageData(t,e){var i=e.targetCanvas.getContext("2d"),r=e.destinationWidth,n=e.destinationHeight,s=r*n*4,o=new Uint8Array(this.imageBuffer,0,s),a=new Uint8ClampedArray(this.imageBuffer,0,s);t.readPixels(0,0,r,n,t.RGBA,t.UNSIGNED_BYTE,o);var c=new ImageData(a,r,n);i.putImageData(c,0,0)}fabric.isTouchSupported="ontouchstart"in fabric.window||"ontouchstart"in fabric.document||fabric.window&&fabric.window.navigator&&0_)for(var C=1,S=d.length;Ct[i-2].x?1:n.x===t[i-2].x?0:-1,c=n.y>t[i-2].y?1:n.y===t[i-2].y?0:-1),r.push(["L",n.x+a*e,n.y+c*e]),r},fabric.util.getPathSegmentsInfo=l,fabric.util.getBoundsOfCurve=function(t,e,i,r,n,s,o,a){var c;if(fabric.cachesBoundsOfCurve&&(c=D.call(arguments),fabric.boundsOfCurveCache[c]))return fabric.boundsOfCurveCache[c];var h,l,u,f,d,g,p,v,m=Math.sqrt,b=Math.min,y=Math.max,_=Math.abs,x=[],C=[[],[]];l=6*t-12*i+6*n,h=-3*t+9*i-9*n+3*o,u=3*i-3*t;for(var S=0;S<2;++S)if(0/g,">")},graphemeSplit:function(t){var e,i=0,r=[];for(i=0;it.x&&this.y>t.y},gte:function(t){return this.x>=t.x&&this.y>=t.y},lerp:function(t,e){return void 0===e&&(e=.5),e=Math.max(Math.min(1,e),0),new i(this.x+(t.x-this.x)*e,this.y+(t.y-this.y)*e)},distanceFrom:function(t){var e=this.x-t.x,i=this.y-t.y;return Math.sqrt(e*e+i*i)},midPointFrom:function(t){return this.lerp(t)},min:function(t){return new i(Math.min(this.x,t.x),Math.min(this.y,t.y))},max:function(t){return new i(Math.max(this.x,t.x),Math.max(this.y,t.y))},toString:function(){return this.x+","+this.y},setXY:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setFromPoint:function(t){return this.x=t.x,this.y=t.y,this},swap:function(t){var e=this.x,i=this.y;this.x=t.x,this.y=t.y,t.x=e,t.y=i},clone:function(){return new i(this.x,this.y)}}}("undefined"!=typeof exports?exports:this),function(t){"use strict";var f=t.fabric||(t.fabric={});function d(t){this.status=t,this.points=[]}f.Intersection?f.warn("fabric.Intersection is already defined"):(f.Intersection=d,f.Intersection.prototype={constructor:d,appendPoint:function(t){return this.points.push(t),this},appendPoints:function(t){return this.points=this.points.concat(t),this}},f.Intersection.intersectLineLine=function(t,e,i,r){var n,s=(r.x-i.x)*(t.y-i.y)-(r.y-i.y)*(t.x-i.x),o=(e.x-t.x)*(t.y-i.y)-(e.y-t.y)*(t.x-i.x),a=(r.y-i.y)*(e.x-t.x)-(r.x-i.x)*(e.y-t.y);if(0!==a){var c=s/a,h=o/a;0<=c&&c<=1&&0<=h&&h<=1?(n=new d("Intersection")).appendPoint(new f.Point(t.x+c*(e.x-t.x),t.y+c*(e.y-t.y))):n=new d}else n=new d(0===s||0===o?"Coincident":"Parallel");return n},f.Intersection.intersectLinePolygon=function(t,e,i){var r,n,s,o,a=new d,c=i.length;for(o=0;o=c&&(h.x-=c),h.x<=-c&&(h.x+=c),h.y>=c&&(h.y-=c),h.y<=c&&(h.y+=c),h.x-=o.offsetX,h.y-=o.offsetY,h}function y(t){return t.flipX!==t.flipY}function _(t,e,i,r,n){if(0!==t[e]){var s=n/t._getTransformedDimensions()[r]*t[i];t.set(i,s)}}function x(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(0,s.skewY),a=P(e,e.originX,e.originY,i,r),c=Math.abs(2*a.x)-o.x,h=s.skewX;c<2?n=0:(n=v(Math.atan2(c/s.scaleX,o.y/s.scaleY)),e.originX===f&&e.originY===p&&(n=-n),e.originX===g&&e.originY===d&&(n=-n),y(s)&&(n=-n));var l=h!==n;if(l){var u=s._getTransformedDimensions().y;s.set("skewX",n),_(s,"skewY","scaleY","y",u)}return l}function C(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(s.skewX,0),a=P(e,e.originX,e.originY,i,r),c=Math.abs(2*a.y)-o.y,h=s.skewY;c<2?n=0:(n=v(Math.atan2(c/s.scaleY,o.x/s.scaleX)),e.originX===f&&e.originY===p&&(n=-n),e.originX===g&&e.originY===d&&(n=-n),y(s)&&(n=-n));var l=h!==n;if(l){var u=s._getTransformedDimensions().x;s.set("skewY",n),_(s,"skewX","scaleX","x",u)}return l}function E(t,e,i,r,n){n=n||{};var s,o,a,c,h,l,u=e.target,f=u.lockScalingX,d=u.lockScalingY,g=n.by,p=w(t,u),v=k(u,g,p),m=e.gestureScale;if(v)return!1;if(m)o=e.scaleX*m,a=e.scaleY*m;else{if(s=P(e,e.originX,e.originY,i,r),h="y"!==g?T(s.x):1,l="x"!==g?T(s.y):1,e.signX||(e.signX=h),e.signY||(e.signY=l),u.lockScalingFlip&&(e.signX!==h||e.signY!==l))return!1;if(c=u._getTransformedDimensions(),p&&!g){var b=Math.abs(s.x)+Math.abs(s.y),y=e.original,_=b/(Math.abs(c.x*y.scaleX/u.scaleX)+Math.abs(c.y*y.scaleY/u.scaleY));o=y.scaleX*_,a=y.scaleY*_}else o=Math.abs(s.x*u.scaleX/c.x),a=Math.abs(s.y*u.scaleY/c.y);O(e)&&(o*=2,a*=2),e.signX!==h&&"y"!==g&&(e.originX=S[e.originX],o*=-1,e.signX=h),e.signY!==l&&"x"!==g&&(e.originY=S[e.originY],a*=-1,e.signY=l)}var x=u.scaleX,C=u.scaleY;return g?("x"===g&&u.set("scaleX",o),"y"===g&&u.set("scaleY",a)):(!f&&u.set("scaleX",o),!d&&u.set("scaleY",a)),x!==u.scaleX||C!==u.scaleY}n.scaleCursorStyleHandler=function(t,e,i){var r=w(t,i),n="";if(0!==e.x&&0===e.y?n="x":0===e.x&&0!==e.y&&(n="y"),k(i,n,r))return"not-allowed";var s=a(i,e);return o[s]+"-resize"},n.skewCursorStyleHandler=function(t,e,i){var r="not-allowed";if(0!==e.x&&i.lockSkewingY)return r;if(0!==e.y&&i.lockSkewingX)return r;var n=a(i,e)%4;return s[n]+"-resize"},n.scaleSkewCursorStyleHandler=function(t,e,i){return t[i.canvas.altActionKey]?n.skewCursorStyleHandler(t,e,i):n.scaleCursorStyleHandler(t,e,i)},n.rotationWithSnapping=b("rotating",m(function(t,e,i,r){var n=e,s=n.target,o=s.translateToOriginPoint(s.getCenterPoint(),n.originX,n.originY);if(s.lockRotation)return!1;var a,c=Math.atan2(n.ey-o.y,n.ex-o.x),h=Math.atan2(r-o.y,i-o.x),l=v(h-c+n.theta);if(0o.r2,h=this.gradientTransform?this.gradientTransform.concat():fabric.iMatrix.concat(),l=-this.offsetX,u=-this.offsetY,f=!!e.additionalTransform,d="pixels"===this.gradientUnits?"userSpaceOnUse":"objectBoundingBox";if(a.sort(function(t,e){return t.offset-e.offset}),"objectBoundingBox"===d?(l/=t.width,u/=t.height):(l+=t.width/2,u+=t.height/2),"path"===t.type&&"percentage"!==this.gradientUnits&&(l-=t.pathOffset.x,u-=t.pathOffset.y),h[4]-=l,h[5]-=u,s='id="SVGID_'+this.id+'" gradientUnits="'+d+'"',s+=' gradientTransform="'+(f?e.additionalTransform+" ":"")+fabric.util.matrixToSVG(h)+'" ',"linear"===this.type?n=["\n']:"radial"===this.type&&(n=["\n']),"radial"===this.type){if(c)for((a=a.concat()).reverse(),i=0,r=a.length;i\n')}return n.push("linear"===this.type?"\n":"\n"),n.join("")},toLive:function(t){var e,i,r,n=fabric.util.object.clone(this.coords);if(this.type){for("linear"===this.type?e=t.createLinearGradient(n.x1,n.y1,n.x2,n.y2):"radial"===this.type&&(e=t.createRadialGradient(n.x1,n.y1,n.r1,n.x2,n.y2,n.r2)),i=0,r=this.colorStops.length;i\n\n\n'},setOptions:function(t){for(var e in t)this[e]=t[e]},toLive:function(t){var e=this.source;if(!e)return"";if(void 0!==e.src){if(!e.complete)return"";if(0===e.naturalWidth||0===e.naturalHeight)return""}return t.createPattern(e,this.repeat)}})}(),function(t){"use strict";var o=t.fabric||(t.fabric={}),a=o.util.toFixed;o.Shadow?o.warn("fabric.Shadow is already defined."):(o.Shadow=o.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,nonScaling:!1,initialize:function(t){for(var e in"string"==typeof t&&(t=this._parseShadow(t)),t)this[e]=t[e];this.id=o.Object.__uid++},_parseShadow:function(t){var e=t.trim(),i=o.Shadow.reOffsetsAndBlur.exec(e)||[];return{color:(e.replace(o.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)").trim(),offsetX:parseFloat(i[1],10)||0,offsetY:parseFloat(i[2],10)||0,blur:parseFloat(i[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(t){var e=40,i=40,r=o.Object.NUM_FRACTION_DIGITS,n=o.util.rotateVector({x:this.offsetX,y:this.offsetY},o.util.degreesToRadians(-t.angle)),s=new o.Color(this.color);return t.width&&t.height&&(e=100*a((Math.abs(n.x)+this.blur)/t.width,r)+20,i=100*a((Math.abs(n.y)+this.blur)/t.height,r)+20),t.flipX&&(n.x*=-1),t.flipY&&(n.y*=-1),'\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY,affectStroke:this.affectStroke,nonScaling:this.nonScaling};var e={},i=o.Shadow.prototype;return["color","blur","offsetX","offsetY","affectStroke","nonScaling"].forEach(function(t){this[t]!==i[t]&&(e[t]=this[t])},this),e}}),o.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/)}("undefined"!=typeof exports?exports:this),function(){"use strict";if(fabric.StaticCanvas)fabric.warn("fabric.StaticCanvas is already defined.");else{var n=fabric.util.object.extend,t=fabric.util.getElementOffset,h=fabric.util.removeFromArray,a=fabric.util.toFixed,s=fabric.util.transformPoint,o=fabric.util.invertTransform,i=fabric.util.getNodeCanvas,r=fabric.util.createCanvasElement,e=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass(fabric.CommonMethods,{initialize:function(t,e){e||(e={}),this.renderAndResetBound=this.renderAndReset.bind(this),this.requestRenderAllBound=this.requestRenderAll.bind(this),this._initStatic(t,e)},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!1,renderOnAddRemove:!0,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,viewportTransform:fabric.iMatrix.concat(),backgroundVpt:!0,overlayVpt:!0,enableRetinaScaling:!0,vptCoords:{},skipOffscreen:!0,clipPath:void 0,_initStatic:function(t,e){var i=this.requestRenderAllBound;this._objects=[],this._createLowerCanvas(t),this._initOptions(e),this.interactive||this._initRetinaScaling(),e.overlayImage&&this.setOverlayImage(e.overlayImage,i),e.backgroundImage&&this.setBackgroundImage(e.backgroundImage,i),e.backgroundColor&&this.setBackgroundColor(e.backgroundColor,i),e.overlayColor&&this.setOverlayColor(e.overlayColor,i),this.calcOffset()},_isRetinaScaling:function(){return 1\n'),this._setSVGBgOverlayColor(i,"background"),this._setSVGBgOverlayImage(i,"backgroundImage",e),this._setSVGObjects(i,e),this.clipPath&&i.push("\n"),this._setSVGBgOverlayColor(i,"overlay"),this._setSVGBgOverlayImage(i,"overlayImage",e),i.push(""),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,r=e.width||this.width,n=e.height||this.height,s='viewBox="0 0 '+this.width+" "+this.height+'" ',o=fabric.Object.NUM_FRACTION_DIGITS;e.viewBox?s='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,s='viewBox="'+a(-i[4]/i[0],o)+" "+a(-i[5]/i[3],o)+" "+a(this.width/i[0],o)+" "+a(this.height/i[3],o)+'" '),t.push("\n',"Created with Fabric.js ",fabric.version,"\n","\n",this.createSVGFontFacesMarkup(),this.createSVGRefElementsMarkup(),this.createSVGClipPathMarkup(e),"\n")},createSVGClipPathMarkup:function(t){var e=this.clipPath;return e?(e.clipPathId="CLIPPATH_"+fabric.Object.__uid++,'\n'+this.clipPath.toClipPathSVG(t.reviver)+"\n"):""},createSVGRefElementsMarkup:function(){var s=this;return["background","overlay"].map(function(t){var e=s[t+"Color"];if(e&&e.toLive){var i=s[t+"Vpt"],r=s.viewportTransform,n={width:s.width/(i?r[0]:1),height:s.height/(i?r[3]:1)};return e.toSVG(n,{additionalTransform:i?fabric.util.matrixToSVG(r):""})}}).join("")},createSVGFontFacesMarkup:function(){var t,e,i,r,n,s,o,a,c="",h={},l=fabric.fontPaths,u=[];for(this._objects.forEach(function t(e){u.push(e),e._objects&&e._objects.forEach(t)}),o=0,a=u.length;o',"\n",c,"","\n"].join("")),c},_setSVGObjects:function(t,e){var i,r,n,s=this._objects;for(r=0,n=s.length;r\n")}else t.push('\n")},sendToBack:function(t){if(!t)return this;var e,i,r,n=this._activeObject;if(t===n&&"activeSelection"===t.type)for(e=(r=n._objects).length;e--;)i=r[e],h(this._objects,i),this._objects.unshift(i);else h(this._objects,t),this._objects.unshift(t);return this.renderOnAddRemove&&this.requestRenderAll(),this},bringToFront:function(t){if(!t)return this;var e,i,r,n=this._activeObject;if(t===n&&"activeSelection"===t.type)for(r=n._objects,e=0;e"}}),n(fabric.StaticCanvas.prototype,fabric.Observable),n(fabric.StaticCanvas.prototype,fabric.Collection),n(fabric.StaticCanvas.prototype,fabric.DataURLExporter),n(fabric.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(t){var e=r();if(!e||!e.getContext)return null;var i=e.getContext("2d");if(!i)return null;switch(t){case"setLineDash":return void 0!==i.setLineDash;default:return null}}}),fabric.StaticCanvas.prototype.toJSON=fabric.StaticCanvas.prototype.toObject,fabric.isLikelyNode&&(fabric.StaticCanvas.prototype.createPNGStream=function(){var t=i(this.lowerCanvasEl);return t&&t.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(t){var e=i(this.lowerCanvasEl);return e&&e.createJPEGStream(t)})}}(),fabric.BaseBrush=fabric.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",strokeMiterLimit:10,strokeDashArray:null,limitedToCanvasSize:!1,_setBrushStyles:function(t){t.strokeStyle=this.color,t.lineWidth=this.width,t.lineCap=this.strokeLineCap,t.miterLimit=this.strokeMiterLimit,t.lineJoin=this.strokeLineJoin,t.setLineDash(this.strokeDashArray||[])},_saveAndTransform:function(t){var e=this.canvas.viewportTransform;t.save(),t.transform(e[0],e[1],e[2],e[3],e[4],e[5])},_setShadow:function(){if(this.shadow){var t=this.canvas,e=this.shadow,i=t.contextTop,r=t.getZoom();t&&t._isRetinaScaling()&&(r*=fabric.devicePixelRatio),i.shadowColor=e.color,i.shadowBlur=e.blur*r,i.shadowOffsetX=e.offsetX*r,i.shadowOffsetY=e.offsetY*r}},needsFullRender:function(){return new fabric.Color(this.color).getAlpha()<1||!!this.shadow},_resetShadow:function(){var t=this.canvas.contextTop;t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0},_isOutSideCanvas:function(t){return t.x<0||t.x>this.canvas.getWidth()||t.y<0||t.y>this.canvas.getHeight()}}),fabric.PencilBrush=fabric.util.createClass(fabric.BaseBrush,{decimate:.4,drawStraightLine:!1,straightLineKey:"shiftKey",initialize:function(t){this.canvas=t,this._points=[]},needsFullRender:function(){return this.callSuper("needsFullRender")||this._hasStraightLine},_drawSegment:function(t,e,i){var r=e.midPointFrom(i);return t.quadraticCurveTo(e.x,e.y,r.x,r.y),r},onMouseDown:function(t,e){this.canvas._isMainEvent(e.e)&&(this.drawStraightLine=e.e[this.straightLineKey],this._prepareForDrawing(t),this._captureDrawingPath(t),this._render())},onMouseMove:function(t,e){if(this.canvas._isMainEvent(e.e)&&(this.drawStraightLine=e.e[this.straightLineKey],(!0!==this.limitedToCanvasSize||!this._isOutSideCanvas(t))&&this._captureDrawingPath(t)&&1"},getObjectScaling:function(){if(!this.group)return{scaleX:this.scaleX,scaleY:this.scaleY};var t=x.util.qrDecompose(this.calcTransformMatrix());return{scaleX:Math.abs(t.scaleX),scaleY:Math.abs(t.scaleY)}},getTotalObjectScaling:function(){var t=this.getObjectScaling(),e=t.scaleX,i=t.scaleY;if(this.canvas){var r=this.canvas.getZoom(),n=this.canvas.getRetinaScaling();e*=r*n,i*=r*n}return{scaleX:e,scaleY:i}},getObjectOpacity:function(){var t=this.opacity;return this.group&&(t*=this.group.getObjectOpacity()),t},_set:function(t,e){var i="scaleX"===t||"scaleY"===t,r=this[t]!==e,n=!1;return i&&(e=this._constrainScale(e)),"scaleX"===t&&e<0?(this.flipX=!this.flipX,e*=-1):"scaleY"===t&&e<0?(this.flipY=!this.flipY,e*=-1):"shadow"!==t||!e||e instanceof x.Shadow?"dirty"===t&&this.group&&this.group.set("dirty",e):e=new x.Shadow(e),this[t]=e,r&&(n=this.group&&this.group.isOnACache(),-1=t.x&&n.left+n.width<=e.x&&n.top>=t.y&&n.top+n.height<=e.y},containsPoint:function(t,e,i,r){var n=this._getCoords(i,r),s=(e=e||this._getImageLines(n),this._findCrossPoints(t,e));return 0!==s&&s%2==1},isOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.getCoords(!0,t).some(function(t){return t.x<=i.x&&t.x>=e.x&&t.y<=i.y&&t.y>=e.y})||(!!this.intersectsWithRect(e,i,!0,t)||this._containsCenterOfCanvas(e,i,t))},_containsCenterOfCanvas:function(t,e,i){var r={x:(t.x+e.x)/2,y:(t.y+e.y)/2};return!!this.containsPoint(r,null,!0,i)},isPartiallyOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.intersectsWithRect(e,i,!0,t)||this.getCoords(!0,t).every(function(t){return(t.x>=i.x||t.x<=e.x)&&(t.y>=i.y||t.y<=e.y)})&&this._containsCenterOfCanvas(e,i,t)},_getImageLines:function(t){return{topline:{o:t.tl,d:t.tr},rightline:{o:t.tr,d:t.br},bottomline:{o:t.br,d:t.bl},leftline:{o:t.bl,d:t.tl}}},_findCrossPoints:function(t,e){var i,r,n,s=0;for(var o in e)if(!((n=e[o]).o.y=t.y&&n.d.y>=t.y||(n.o.x===n.d.x&&n.o.x>=t.x?r=n.o.x:(0,i=(n.d.y-n.o.y)/(n.d.x-n.o.x),r=-(t.y-0*t.x-(n.o.y-i*n.o.x))/(0-i)),r>=t.x&&(s+=1),2!==s)))break;return s},getBoundingRect:function(t,e){var i=this.getCoords(t,e);return h.makeBoundingBoxFromPoints(i)},getScaledWidth:function(){return this._getTransformedDimensions().x},getScaledHeight:function(){return this._getTransformedDimensions().y},_constrainScale:function(t){return Math.abs(t)\n')}},toSVG:function(t){return this._createBaseSVGMarkup(this._toSVG(t),{reviver:t})},toClipPathSVG:function(t){return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(t),{reviver:t})},_createBaseClipPathSVGMarkup:function(t,e){var i=(e=e||{}).reviver,r=e.additionalTransform||"",n=[this.getSvgTransform(!0,r),this.getSvgCommons()].join(""),s=t.indexOf("COMMON_PARTS");return t[s]=n,i?i(t.join("")):t.join("")},_createBaseSVGMarkup:function(t,e){var i,r,n=(e=e||{}).noStyle,s=e.reviver,o=n?"":'style="'+this.getSvgStyles()+'" ',a=e.withShadow?'style="'+this.getSvgFilter()+'" ':"",c=this.clipPath,h=this.strokeUniform?'vector-effect="non-scaling-stroke" ':"",l=c&&c.absolutePositioned,u=this.stroke,f=this.fill,d=this.shadow,g=[],p=t.indexOf("COMMON_PARTS"),v=e.additionalTransform;return c&&(c.clipPathId="CLIPPATH_"+fabric.Object.__uid++,r='\n'+c.toClipPathSVG(s)+"\n"),l&&g.push("\n"),g.push("\n"),i=[o,h,n?"":this.addPaintOrder()," ",v?'transform="'+v+'" ':""].join(""),t[p]=i,f&&f.toLive&&g.push(f.toSVG(this)),u&&u.toLive&&g.push(u.toSVG(this)),d&&g.push(d.toSVG(this)),c&&g.push(r),g.push(t.join("")),g.push("\n"),l&&g.push("\n"),s?s(g.join("")):g.join("")},addPaintOrder:function(){return"fill"!==this.paintFirst?' paint-order="'+this.paintFirst+'" ':""}})}(),function(){var n=fabric.util.object.extend,r="stateProperties";function s(e,t,i){var r={};i.forEach(function(t){r[t]=e[t]}),n(e[t],r,!0)}fabric.util.object.extend(fabric.Object.prototype,{hasStateChanged:function(t){var e="_"+(t=t||r);return Object.keys(this[e]).length\n']}}),s.Line.ATTRIBUTE_NAMES=s.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),s.Line.fromElement=function(t,e,i){i=i||{};var r=s.parseAttributes(t,s.Line.ATTRIBUTE_NAMES),n=[r.x1||0,r.y1||0,r.x2||0,r.y2||0];e(new s.Line(n,o(r,i)))},s.Line.fromObject=function(t,e){var i=r(t,!0);i.points=[t.x1,t.y1,t.x2,t.y2],s.Object._fromObject("Line",i,function(t){delete t.points,e&&e(t)},"points")})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var s=t.fabric||(t.fabric={}),o=s.util.degreesToRadians;s.Circle?s.warn("fabric.Circle is already defined."):(s.Circle=s.util.createClass(s.Object,{type:"circle",radius:0,startAngle:0,endAngle:360,cacheProperties:s.Object.prototype.cacheProperties.concat("radius","startAngle","endAngle"),_set:function(t,e){return this.callSuper("_set",t,e),"radius"===t&&this.setRadius(e),this},toObject:function(t){return this.callSuper("toObject",["radius","startAngle","endAngle"].concat(t))},_toSVG:function(){var t,e=(this.endAngle-this.startAngle)%360;if(0===e)t=["\n'];else{var i=o(this.startAngle),r=o(this.endAngle),n=this.radius;t=['\n"]}return t},_render:function(t){t.beginPath(),t.arc(0,0,this.radius,o(this.startAngle),o(this.endAngle),!1),this._renderPaintInOrder(t)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(t){return this.radius=t,this.set("width",2*t).set("height",2*t)}}),s.Circle.ATTRIBUTE_NAMES=s.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),s.Circle.fromElement=function(t,e){var i,r=s.parseAttributes(t,s.Circle.ATTRIBUTE_NAMES);if(!("radius"in(i=r)&&0<=i.radius))throw new Error("value of `r` attribute is required and can not be negative");r.left=(r.left||0)-r.radius,r.top=(r.top||0)-r.radius,e(new s.Circle(r))},s.Circle.fromObject=function(t,e){s.Object._fromObject("Circle",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var i=t.fabric||(t.fabric={});i.Triangle?i.warn("fabric.Triangle is already defined"):(i.Triangle=i.util.createClass(i.Object,{type:"triangle",width:100,height:100,_render:function(t){var e=this.width/2,i=this.height/2;t.beginPath(),t.moveTo(-e,i),t.lineTo(0,-i),t.lineTo(e,i),t.closePath(),this._renderPaintInOrder(t)},_toSVG:function(){var t=this.width/2,e=this.height/2;return["']}}),i.Triangle.fromObject=function(t,e){return i.Object._fromObject("Triangle",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var r=t.fabric||(t.fabric={}),e=2*Math.PI;r.Ellipse?r.warn("fabric.Ellipse is already defined."):(r.Ellipse=r.util.createClass(r.Object,{type:"ellipse",rx:0,ry:0,cacheProperties:r.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this.set("rx",t&&t.rx||0),this.set("ry",t&&t.ry||0)},_set:function(t,e){switch(this.callSuper("_set",t,e),t){case"rx":this.rx=e,this.set("width",2*e);break;case"ry":this.ry=e,this.set("height",2*e)}return this},getRx:function(){return this.get("rx")*this.get("scaleX")},getRy:function(){return this.get("ry")*this.get("scaleY")},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']},_render:function(t){t.beginPath(),t.save(),t.transform(1,0,0,this.ry/this.rx,0,0),t.arc(0,0,this.rx,0,e,!1),t.restore(),this._renderPaintInOrder(t)}}),r.Ellipse.ATTRIBUTE_NAMES=r.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),r.Ellipse.fromElement=function(t,e){var i=r.parseAttributes(t,r.Ellipse.ATTRIBUTE_NAMES);i.left=(i.left||0)-i.rx,i.top=(i.top||0)-i.ry,e(new r.Ellipse(i))},r.Ellipse.fromObject=function(t,e){r.Object._fromObject("Ellipse",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var s=t.fabric||(t.fabric={}),o=s.util.object.extend;s.Rect?s.warn("fabric.Rect is already defined"):(s.Rect=s.util.createClass(s.Object,{stateProperties:s.Object.prototype.stateProperties.concat("rx","ry"),type:"rect",rx:0,ry:0,cacheProperties:s.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this._initRxRy()},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(t){var e=this.rx?Math.min(this.rx,this.width/2):0,i=this.ry?Math.min(this.ry,this.height/2):0,r=this.width,n=this.height,s=-this.width/2,o=-this.height/2,a=0!==e||0!==i,c=.4477152502;t.beginPath(),t.moveTo(s+e,o),t.lineTo(s+r-e,o),a&&t.bezierCurveTo(s+r-c*e,o,s+r,o+c*i,s+r,o+i),t.lineTo(s+r,o+n-i),a&&t.bezierCurveTo(s+r,o+n-c*i,s+r-c*e,o+n,s+r-e,o+n),t.lineTo(s+e,o+n),a&&t.bezierCurveTo(s+c*e,o+n,s,o+n-c*i,s,o+n-i),t.lineTo(s,o+i),a&&t.bezierCurveTo(s,o+c*i,s+c*e,o,s+e,o),t.closePath(),this._renderPaintInOrder(t)},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']}}),s.Rect.ATTRIBUTE_NAMES=s.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),s.Rect.fromElement=function(t,e,i){if(!t)return e(null);i=i||{};var r=s.parseAttributes(t,s.Rect.ATTRIBUTE_NAMES);r.left=r.left||0,r.top=r.top||0,r.height=r.height||0,r.width=r.width||0;var n=new s.Rect(o(i?s.util.object.clone(i):{},r));n.visible=n.visible&&0\n']},commonRender:function(t){var e,i=this.points.length,r=this.pathOffset.x,n=this.pathOffset.y;if(!i||isNaN(this.points[i-1].y))return!1;t.beginPath(),t.moveTo(this.points[0].x-r,this.points[0].y-n);for(var s=0;s"},toObject:function(t){return n(this.callSuper("toObject",t),{path:this.path.map(function(t){return t.slice()})})},toDatalessObject:function(t){var e=this.toObject(["sourcePath"].concat(t));return e.sourcePath&&delete e.path,e},_toSVG:function(){return["\n"]},_getOffsetTransform:function(){var t=f.Object.NUM_FRACTION_DIGITS;return" translate("+e(-this.pathOffset.x,t)+", "+e(-this.pathOffset.y,t)+")"},toClipPathSVG:function(t){var e=this._getOffsetTransform();return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},toSVG:function(t){var e=this._getOffsetTransform();return this._createBaseSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},complexity:function(){return this.path.length},_calcDimensions:function(){for(var t,e,i=[],r=[],n=0,s=0,o=0,a=0,c=0,h=this.path.length;c"},addWithUpdate:function(t){var e=!!this.group;return this._restoreObjectsState(),h.util.resetObjectTransform(this),t&&(e&&h.util.removeTransformFromObject(t,this.group.calcTransformMatrix()),this._objects.push(t),t.group=this,t._set("canvas",this.canvas)),this._calcBounds(),this._updateObjectsCoords(),this.dirty=!0,e?this.group.addWithUpdate():this.setCoords(),this},removeWithUpdate:function(t){return this._restoreObjectsState(),h.util.resetObjectTransform(this),this.remove(t),this._calcBounds(),this._updateObjectsCoords(),this.setCoords(),this.dirty=!0,this},_onObjectAdded:function(t){this.dirty=!0,t.group=this,t._set("canvas",this.canvas)},_onObjectRemoved:function(t){this.dirty=!0,delete t.group},_set:function(t,e){var i=this._objects.length;if(this.useSetOnGroup)for(;i--;)this._objects[i].setOnGroup(t,e);if("canvas"===t)for(;i--;)this._objects[i]._set(t,e);h.Object.prototype._set.call(this,t,e)},toObject:function(r){var n=this.includeDefaultValues,t=this._objects.filter(function(t){return!t.excludeFromExport}).map(function(t){var e=t.includeDefaultValues;t.includeDefaultValues=n;var i=t.toObject(r);return t.includeDefaultValues=e,i}),e=h.Object.prototype.toObject.call(this,r);return e.objects=t,e},toDatalessObject:function(r){var t,e=this.sourcePath;if(e)t=e;else{var n=this.includeDefaultValues;t=this._objects.map(function(t){var e=t.includeDefaultValues;t.includeDefaultValues=n;var i=t.toDatalessObject(r);return t.includeDefaultValues=e,i})}var i=h.Object.prototype.toDatalessObject.call(this,r);return i.objects=t,i},render:function(t){this._transformDone=!0,this.callSuper("render",t),this._transformDone=!1},shouldCache:function(){var t=h.Object.prototype.shouldCache.call(this);if(t)for(var e=0,i=this._objects.length;e\n"],i=0,r=this._objects.length;i\n"),e},getSvgStyles:function(){var t=void 0!==this.opacity&&1!==this.opacity?"opacity: "+this.opacity+";":"",e=this.visible?"":" visibility: hidden;";return[t,this.getSvgFilter(),e].join("")},toClipPathSVG:function(t){for(var e=[],i=0,r=this._objects.length;i"},shouldCache:function(){return!1},isOnACache:function(){return!1},_renderControls:function(t,e,i){t.save(),t.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,this.callSuper("_renderControls",t,e),void 0===(i=i||{}).hasControls&&(i.hasControls=!1),i.forActiveSelection=!0;for(var r=0,n=this._objects.length;r\n','\t\n',"\n"),o=' clip-path="url(#imageCrop_'+c+')" '}if(this.imageSmoothing||(a='" image-rendering="optimizeSpeed'),i.push("\t\n"),this.stroke||this.strokeDashArray){var h=this.fill;this.fill=null,t=["\t\n'],this.fill=h}return e="fill"!==this.paintFirst?e.concat(t,i):e.concat(i,t)},getSrc:function(t){var e=t?this._element:this._originalElement;return e?e.toDataURL?e.toDataURL():this.srcFromAttribute?e.getAttribute("src"):e.src:this.src||""},setSrc:function(t,i,r){return fabric.util.loadImage(t,function(t,e){this.setElement(t,r),this._setWidthHeight(),i&&i(this,e)},this,r&&r.crossOrigin),this},toString:function(){return'#'},applyResizeFilters:function(){var t=this.resizeFilter,e=this.minimumScaleTrigger,i=this.getTotalObjectScaling(),r=i.scaleX,n=i.scaleY,s=this._filteredEl||this._originalElement;if(this.group&&this.set("dirty",!0),!t||e=t;for(var a=["highp","mediump","lowp"],c=0;c<3;c++)if(void 0,i="precision "+a[c]+" float;\nvoid main(){}",r=(e=s).createShader(e.FRAGMENT_SHADER),e.shaderSource(r,i),e.compileShader(r),e.getShaderParameter(r,e.COMPILE_STATUS)){fabric.webGlPrecision=a[c];break}}return this.isSupported=o},(fabric.WebglFilterBackend=t).prototype={tileSize:2048,resources:{},setupGLContext:function(t,e){this.dispose(),this.createWebGLCanvas(t,e),this.aPosition=new Float32Array([0,0,0,1,1,0,1,1]),this.chooseFastestCopyGLTo2DMethod(t,e)},chooseFastestCopyGLTo2DMethod:function(t,e){var i,r=void 0!==window.performance;try{new ImageData(1,1),i=!0}catch(t){i=!1}var n="undefined"!=typeof ArrayBuffer,s="undefined"!=typeof Uint8ClampedArray;if(r&&i&&n&&s){var o=fabric.util.createCanvasElement(),a=new ArrayBuffer(t*e*4);if(fabric.forceGLPutImageData)return this.imageBuffer=a,void(this.copyGLTo2D=copyGLTo2DPutImageData);var c,h,l={imageBuffer:a,destinationWidth:t,destinationHeight:e,targetCanvas:o};o.width=t,o.height=e,c=window.performance.now(),copyGLTo2DDrawImage.call(l,this.gl,l),h=window.performance.now()-c,c=window.performance.now(),copyGLTo2DPutImageData.call(l,this.gl,l),window.performance.now()-c 0.0) {\n"+this.fragmentSource[t]+"}\n}"},retrieveShader:function(t){var e,i=this.type+"_"+this.mode;return t.programCache.hasOwnProperty(i)||(e=this.buildSource(this.mode),t.programCache[i]=this.createProgram(t.context,e)),t.programCache[i]},applyTo2d:function(t){var e,i,r,n,s,o,a,c=t.imageData.data,h=c.length,l=1-this.alpha;e=(a=new f.Color(this.color).getSource())[0]*this.alpha,i=a[1]*this.alpha,r=a[2]*this.alpha;for(var u=0;u'},_getCacheCanvasDimensions:function(){var t=this.callSuper("_getCacheCanvasDimensions"),e=this.fontSize;return t.width+=e*t.zoomX,t.height+=e*t.zoomY,t},_render:function(t){var e=this.path;e&&!e.isNotVisible()&&e._render(t),this._setTextStyles(t),this._renderTextLinesBackground(t),this._renderTextDecoration(t,"underline"),this._renderText(t),this._renderTextDecoration(t,"overline"),this._renderTextDecoration(t,"linethrough")},_renderText:function(t){"stroke"===this.paintFirst?(this._renderTextStroke(t),this._renderTextFill(t)):(this._renderTextFill(t),this._renderTextStroke(t))},_setTextStyles:function(t,e,i){if(t.textBaseline="alphabetical",this.path)switch(this.pathAlign){case"center":t.textBaseline="middle";break;case"ascender":t.textBaseline="top";break;case"descender":t.textBaseline="bottom"}t.font=this._getFontDeclaration(e,i)},calcTextWidth:function(){for(var t=this.getLineWidth(0),e=1,i=this._textLines.length;ethis.__selectionStartOnMouseDown?(this.selectionStart=this.__selectionStartOnMouseDown,this.selectionEnd=e):(this.selectionStart=e,this.selectionEnd=this.__selectionStartOnMouseDown),this.selectionStart===i&&this.selectionEnd===r||(this.restartCursorIfNeeded(),this._fireSelectionChanged(),this._updateTextarea(),this.renderCursorOrSelection()))}},_setEditingProps:function(){this.hoverCursor="text",this.canvas&&(this.canvas.defaultCursor=this.canvas.moveCursor="text"),this.borderColor=this.editingBorderColor,this.hasControls=this.selectable=!1,this.lockMovementX=this.lockMovementY=!0},fromStringToGraphemeSelection:function(t,e,i){var r=i.slice(0,t),n=fabric.util.string.graphemeSplit(r).length;if(t===e)return{selectionStart:n,selectionEnd:n};var s=i.slice(t,e);return{selectionStart:n,selectionEnd:n+fabric.util.string.graphemeSplit(s).length}},fromGraphemeToStringSelection:function(t,e,i){var r=i.slice(0,t).join("").length;return t===e?{selectionStart:r,selectionEnd:r}:{selectionStart:r,selectionEnd:r+i.slice(t,e).join("").length}},_updateTextarea:function(){if(this.cursorOffsetCache={},this.hiddenTextarea){if(!this.inCompositionMode){var t=this.fromGraphemeToStringSelection(this.selectionStart,this.selectionEnd,this._text);this.hiddenTextarea.selectionStart=t.selectionStart,this.hiddenTextarea.selectionEnd=t.selectionEnd}this.updateTextareaPosition()}},updateFromTextArea:function(){if(this.hiddenTextarea){this.cursorOffsetCache={},this.text=this.hiddenTextarea.value,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords());var t=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value);this.selectionEnd=this.selectionStart=t.selectionEnd,this.inCompositionMode||(this.selectionStart=t.selectionStart),this.updateTextareaPosition()}},updateTextareaPosition:function(){if(this.selectionStart===this.selectionEnd){var t=this._calcTextareaPosition();this.hiddenTextarea.style.left=t.left,this.hiddenTextarea.style.top=t.top}},_calcTextareaPosition:function(){if(!this.canvas)return{x:1,y:1};var t=this.inCompositionMode?this.compositionStart:this.selectionStart,e=this._getCursorBoundaries(t),i=this.get2DCursorLocation(t),r=i.lineIndex,n=i.charIndex,s=this.getValueOfPropertyAt(r,n,"fontSize")*this.lineHeight,o=e.leftOffset,a=this.calcTransformMatrix(),c={x:e.left+o,y:e.top+e.topOffset+s},h=this.canvas.getRetinaScaling(),l=this.canvas.upperCanvasEl,u=l.width/h,f=l.height/h,d=u-s,g=f-s,p=l.clientWidth/u,v=l.clientHeight/f;return c=fabric.util.transformPoint(c,a),(c=fabric.util.transformPoint(c,this.canvas.viewportTransform)).x*=p,c.y*=v,c.x<0&&(c.x=0),c.x>d&&(c.x=d),c.y<0&&(c.y=0),c.y>g&&(c.y=g),c.x+=this.canvas._offset.left,c.y+=this.canvas._offset.top,{left:c.x+"px",top:c.y+"px",fontSize:s+"px",charHeight:s}},_saveEditingProps:function(){this._savedProps={hasControls:this.hasControls,borderColor:this.borderColor,lockMovementX:this.lockMovementX,lockMovementY:this.lockMovementY,hoverCursor:this.hoverCursor,selectable:this.selectable,defaultCursor:this.canvas&&this.canvas.defaultCursor,moveCursor:this.canvas&&this.canvas.moveCursor}},_restoreEditingProps:function(){this._savedProps&&(this.hoverCursor=this._savedProps.hoverCursor,this.hasControls=this._savedProps.hasControls,this.borderColor=this._savedProps.borderColor,this.selectable=this._savedProps.selectable,this.lockMovementX=this._savedProps.lockMovementX,this.lockMovementY=this._savedProps.lockMovementY,this.canvas&&(this.canvas.defaultCursor=this._savedProps.defaultCursor,this.canvas.moveCursor=this._savedProps.moveCursor))},exitEditing:function(){var t=this._textBeforeEdit!==this.text,e=this.hiddenTextarea;return this.selected=!1,this.isEditing=!1,this.selectionEnd=this.selectionStart,e&&(e.blur&&e.blur(),e.parentNode&&e.parentNode.removeChild(e)),this.hiddenTextarea=null,this.abortCursorAnimation(),this._restoreEditingProps(),this._currentCursorOpacity=0,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this.fire("editing:exited"),t&&this.fire("modified"),this.canvas&&(this.canvas.off("mouse:move",this.mouseMoveHandler),this.canvas.fire("text:editing:exited",{target:this}),t&&this.canvas.fire("object:modified",{target:this})),this},_removeExtraneousStyles:function(){for(var t in this.styles)this._textLines[t]||delete this.styles[t]},removeStyleFromTo:function(t,e){var i,r,n=this.get2DCursorLocation(t,!0),s=this.get2DCursorLocation(e,!0),o=n.lineIndex,a=n.charIndex,c=s.lineIndex,h=s.charIndex;if(o!==c){if(this.styles[o])for(i=a;it?this.selectionStart=t:this.selectionStart<0&&(this.selectionStart=0),this.selectionEnd>t?this.selectionEnd=t:this.selectionEnd<0&&(this.selectionEnd=0)}})}(),fabric.util.object.extend(fabric.IText.prototype,{initDoubleClickSimulation:function(){this.__lastClickTime=+new Date,this.__lastLastClickTime=+new Date,this.__lastPointer={},this.on("mousedown",this.onMouseDown)},onMouseDown:function(t){if(this.canvas){this.__newClickTime=+new Date;var e=t.pointer;this.isTripleClick(e)&&(this.fire("tripleclick",t),this._stopEvent(t.e)),this.__lastLastClickTime=this.__lastClickTime,this.__lastClickTime=this.__newClickTime,this.__lastPointer=e,this.__lastIsEditing=this.isEditing,this.__lastSelected=this.selected}},isTripleClick:function(t){return this.__newClickTime-this.__lastClickTime<500&&this.__lastClickTime-this.__lastLastClickTime<500&&this.__lastPointer.x===t.x&&this.__lastPointer.y===t.y},_stopEvent:function(t){t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation()},initCursorSelectionHandlers:function(){this.initMousedownHandler(),this.initMouseupHandler(),this.initClicks()},doubleClickHandler:function(t){this.isEditing&&this.selectWord(this.getSelectionStartFromPointer(t.e))},tripleClickHandler:function(t){this.isEditing&&this.selectLine(this.getSelectionStartFromPointer(t.e))},initClicks:function(){this.on("mousedblclick",this.doubleClickHandler),this.on("tripleclick",this.tripleClickHandler)},_mouseDownHandler:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.__isMousedown=!0,this.selected&&(this.inCompositionMode=!1,this.setCursorByClick(t.e)),this.isEditing&&(this.__selectionStartOnMouseDown=this.selectionStart,this.selectionStart===this.selectionEnd&&this.abortCursorAnimation(),this.renderCursorOrSelection()))},_mouseDownHandlerBefore:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.selected=this===this.canvas._activeObject)},initMousedownHandler:function(){this.on("mousedown",this._mouseDownHandler),this.on("mousedown:before",this._mouseDownHandlerBefore)},initMouseupHandler:function(){this.on("mouseup",this.mouseUpHandler)},mouseUpHandler:function(t){if(this.__isMousedown=!1,!(!this.editable||this.group||t.transform&&t.transform.actionPerformed||t.e.button&&1!==t.e.button)){if(this.canvas){var e=this.canvas._activeObject;if(e&&e!==this)return}this.__lastSelected&&!this.__corner?(this.selected=!1,this.__lastSelected=!1,this.enterEditing(t.e),this.selectionStart===this.selectionEnd?this.initDelayedCursor(!0):this.renderCursorOrSelection()):this.selected=!0}},setCursorByClick:function(t){var e=this.getSelectionStartFromPointer(t),i=this.selectionStart,r=this.selectionEnd;t.shiftKey?this.setSelectionStartEndWithShift(i,r,e):(this.selectionStart=e,this.selectionEnd=e),this.isEditing&&(this._fireSelectionChanged(),this._updateTextarea())},getSelectionStartFromPointer:function(t){for(var e,i=this.getLocalPointer(t),r=0,n=0,s=0,o=0,a=0,c=0,h=this._textLines.length;cthis._text.length&&(a=this._text.length),a}}),fabric.util.object.extend(fabric.IText.prototype,{initHiddenTextarea:function(){this.hiddenTextarea=fabric.document.createElement("textarea"),this.hiddenTextarea.setAttribute("autocapitalize","off"),this.hiddenTextarea.setAttribute("autocorrect","off"),this.hiddenTextarea.setAttribute("autocomplete","off"),this.hiddenTextarea.setAttribute("spellcheck","false"),this.hiddenTextarea.setAttribute("data-fabric-hiddentextarea",""),this.hiddenTextarea.setAttribute("wrap","off");var t=this._calcTextareaPosition();this.hiddenTextarea.style.cssText="position: absolute; top: "+t.top+"; left: "+t.left+"; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; paddingーtop: "+t.fontSize+";",this.hiddenTextareaContainer?this.hiddenTextareaContainer.appendChild(this.hiddenTextarea):fabric.document.body.appendChild(this.hiddenTextarea),fabric.util.addListener(this.hiddenTextarea,"keydown",this.onKeyDown.bind(this)),fabric.util.addListener(this.hiddenTextarea,"keyup",this.onKeyUp.bind(this)),fabric.util.addListener(this.hiddenTextarea,"input",this.onInput.bind(this)),fabric.util.addListener(this.hiddenTextarea,"copy",this.copy.bind(this)),fabric.util.addListener(this.hiddenTextarea,"cut",this.copy.bind(this)),fabric.util.addListener(this.hiddenTextarea,"paste",this.paste.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionstart",this.onCompositionStart.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionupdate",this.onCompositionUpdate.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionend",this.onCompositionEnd.bind(this)),!this._clickHandlerInitialized&&this.canvas&&(fabric.util.addListener(this.canvas.upperCanvasEl,"click",this.onClick.bind(this)),this._clickHandlerInitialized=!0)},keysMap:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorRight",36:"moveCursorLeft",37:"moveCursorLeft",38:"moveCursorUp",39:"moveCursorRight",40:"moveCursorDown"},keysMapRtl:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorLeft",36:"moveCursorRight",37:"moveCursorRight",38:"moveCursorUp",39:"moveCursorLeft",40:"moveCursorDown"},ctrlKeysMapUp:{67:"copy",88:"cut"},ctrlKeysMapDown:{65:"selectAll"},onClick:function(){this.hiddenTextarea&&this.hiddenTextarea.focus()},onKeyDown:function(t){if(this.isEditing){var e="rtl"===this.direction?this.keysMapRtl:this.keysMap;if(t.keyCode in e)this[e[t.keyCode]](t);else{if(!(t.keyCode in this.ctrlKeysMapDown&&(t.ctrlKey||t.metaKey)))return;this[this.ctrlKeysMapDown[t.keyCode]](t)}t.stopImmediatePropagation(),t.preventDefault(),33<=t.keyCode&&t.keyCode<=40?(this.inCompositionMode=!1,this.clearContextTop(),this.renderCursorOrSelection()):this.canvas&&this.canvas.requestRenderAll()}},onKeyUp:function(t){!this.isEditing||this._copyDone||this.inCompositionMode?this._copyDone=!1:t.keyCode in this.ctrlKeysMapUp&&(t.ctrlKey||t.metaKey)&&(this[this.ctrlKeysMapUp[t.keyCode]](t),t.stopImmediatePropagation(),t.preventDefault(),this.canvas&&this.canvas.requestRenderAll())},onInput:function(t){var e=this.fromPaste;if(this.fromPaste=!1,t&&t.stopPropagation(),this.isEditing){var i,r,n,s,o,a=this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,c=this._text.length,h=a.length,l=h-c,u=this.selectionStart,f=this.selectionEnd,d=u!==f;if(""===this.hiddenTextarea.value)return this.styles={},this.updateFromTextArea(),this.fire("changed"),void(this.canvas&&(this.canvas.fire("text:changed",{target:this}),this.canvas.requestRenderAll()));var g=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value),p=u>g.selectionStart;d?(i=this._text.slice(u,f),l+=f-u):h=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorUpOrDown("Down",t)},moveCursorUp:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorUpOrDown("Up",t)},_moveCursorUpOrDown:function(t,e){var i=this["get"+t+"CursorOffset"](e,"right"===this._selectionDirection);e.shiftKey?this.moveCursorWithShift(i):this.moveCursorWithoutShift(i),0!==i&&(this.setSelectionInBoundaries(),this.abortCursorAnimation(),this._currentCursorOpacity=1,this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorWithShift:function(t){var e="left"===this._selectionDirection?this.selectionStart+t:this.selectionEnd+t;return this.setSelectionStartEndWithShift(this.selectionStart,this.selectionEnd,e),0!==t},moveCursorWithoutShift:function(t){return t<0?(this.selectionStart+=t,this.selectionEnd=this.selectionStart):(this.selectionEnd+=t,this.selectionStart=this.selectionEnd),0!==t},moveCursorLeft:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorLeftOrRight("Left",t)},_move:function(t,e,i){var r;if(t.altKey)r=this["findWordBoundary"+i](this[e]);else{if(!t.metaKey&&35!==t.keyCode&&36!==t.keyCode)return this[e]+="Left"===i?-1:1,!0;r=this["findLineBoundary"+i](this[e])}if(void 0!==typeof r&&this[e]!==r)return this[e]=r,!0},_moveLeft:function(t,e){return this._move(t,e,"Left")},_moveRight:function(t,e){return this._move(t,e,"Right")},moveCursorLeftWithoutShift:function(t){var e=!0;return this._selectionDirection="left",this.selectionEnd===this.selectionStart&&0!==this.selectionStart&&(e=this._moveLeft(t,"selectionStart")),this.selectionEnd=this.selectionStart,e},moveCursorLeftWithShift:function(t){return"right"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveLeft(t,"selectionEnd"):0!==this.selectionStart?(this._selectionDirection="left",this._moveLeft(t,"selectionStart")):void 0},moveCursorRight:function(t){this.selectionStart>=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorLeftOrRight("Right",t)},_moveCursorLeftOrRight:function(t,e){var i="moveCursor"+t+"With";this._currentCursorOpacity=1,e.shiftKey?i+="Shift":i+="outShift",this[i](e)&&(this.abortCursorAnimation(),this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorRightWithShift:function(t){return"left"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveRight(t,"selectionStart"):this.selectionEnd!==this._text.length?(this._selectionDirection="right",this._moveRight(t,"selectionEnd")):void 0},moveCursorRightWithoutShift:function(t){var e=!0;return this._selectionDirection="right",this.selectionStart===this.selectionEnd?(e=this._moveRight(t,"selectionStart"),this.selectionEnd=this.selectionStart):this.selectionStart=this.selectionEnd,e},removeChars:function(t,e){void 0===e&&(e=t+1),this.removeStyleFromTo(t,e),this._text.splice(t,e-t),this.text=this._text.join(""),this.set("dirty",!0),this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this._removeExtraneousStyles()},insertChars:function(t,e,i,r){void 0===r&&(r=i),i",t.textSpans.join(""),"\n"]},_getSVGTextAndBg:function(t,e){var i,r=[],n=[],s=t;this._setSVGBg(n);for(var o=0,a=this._textLines.length;o",fabric.util.string.escapeXml(t),""].join("")},_setSVGTextLineText:function(t,e,i,r){var n,s,o,a,c,h=this.getHeightOfLine(e),l=-1!==this.textAlign.indexOf("justify"),u="",f=0,d=this._textLines[e];r+=h*(1-this._fontSizeFraction)/this.lineHeight;for(var g=0,p=d.length-1;g<=p;g++)c=g===p||this.charSpacing,u+=d[g],o=this.__charBounds[e][g],0===f?(i+=o.kernedWidth-o.width,f+=o.width):f+=o.kernedWidth,l&&!c&&this._reSpaceAndTab.test(d[g])&&(c=!0),c||(n=n||this.getCompleteStyleDeclaration(e,g),s=this.getCompleteStyleDeclaration(e,g+1),c=this._hasStyleChangedForSvg(n,s)),c&&(a=this._getStyleDeclaration(e,g)||{},t.push(this._createTextCharSpan(u,a,i,r)),u="",n=s,i+=f,f=0)},_pushTextBgRect:function(t,e,i,r,n,s){var o=fabric.Object.NUM_FRACTION_DIGITS;t.push("\t\t\n')},_setSVGTextLineBg:function(t,e,i,r){for(var n,s,o=this._textLines[e],a=this.getHeightOfLine(e)/this.lineHeight,c=0,h=0,l=this.getValueOfPropertyAt(e,0,"textBackgroundColor"),u=0,f=o.length;uthis.width&&this._set("width",this.dynamicMinWidth),-1!==this.textAlign.indexOf("justify")&&this.enlargeSpaces(),this.height=this.calcTextHeight(),this.saveState({propertySet:"_dimensionAffectingProps"}))},_generateStyleMap:function(t){for(var e=0,i=0,r=0,n={},s=0;sthis.dynamicMinWidth&&(this.dynamicMinWidth=g-v+r),o},isEndOfWrapping:function(t){return!this._styleMap[t+1]||this._styleMap[t+1].line!==this._styleMap[t].line},missingNewlineOffset:function(t){return this.splitByGrapheme?this.isEndOfWrapping(t)?1:0:1},_splitTextIntoLines:function(t){for(var e=b.Text.prototype._splitTextIntoLines.call(this,t),i=this._wrapText(e.lines,this.width),r=new Array(i.length),n=0;n 0); - -/** - * True when in environment that's probably Node.js - * @type boolean - */ -fabric.isLikelyNode = typeof Buffer !== 'undefined' && - typeof window === 'undefined'; - -/* _FROM_SVG_START_ */ -/** - * Attributes parsed from all SVG elements - * @type array - */ -fabric.SHARED_ATTRIBUTES = [ - 'display', - 'transform', - 'fill', 'fill-opacity', 'fill-rule', - 'opacity', - 'stroke', 'stroke-dasharray', 'stroke-linecap', 'stroke-dashoffset', - 'stroke-linejoin', 'stroke-miterlimit', - 'stroke-opacity', 'stroke-width', - 'id', 'paint-order', 'vector-effect', - 'instantiated_by_use', 'clip-path', -]; -/* _FROM_SVG_END_ */ - -/** - * Pixel per Inch as a default value set to 96. Can be changed for more realistic conversion. - */ -fabric.DPI = 96; -fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)'; -fabric.commaWsp = '(?:\\s+,?\\s*|,\\s*)'; -fabric.rePathCommand = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/ig; -fabric.reNonWord = /[ \n\.,;!\?\-]/; -fabric.fontPaths = {}; -fabric.iMatrix = [1, 0, 0, 1, 0, 0]; -fabric.svgNS = 'http://www.w3.org/2000/svg'; - -/** - * Pixel limit for cache canvases. 1Mpx , 4Mpx should be fine. - * @since 1.7.14 - * @type Number - * @default - */ -fabric.perfLimitSizeTotal = 2097152; - -/** - * Pixel limit for cache canvases width or height. IE fixes the maximum at 5000 - * @since 1.7.14 - * @type Number - * @default - */ -fabric.maxCacheSideLimit = 4096; - -/** - * Lowest pixel limit for cache canvases, set at 256PX - * @since 1.7.14 - * @type Number - * @default - */ -fabric.minCacheSideLimit = 256; - -/** - * Cache Object for widths of chars in text rendering. - */ -fabric.charWidthsCache = {}; - -/** - * if webgl is enabled and available, textureSize will determine the size - * of the canvas backend - * @since 2.0.0 - * @type Number - * @default - */ -fabric.textureSize = 2048; - -/** - * When 'true', style information is not retained when copy/pasting text, making - * pasted text use destination style. - * Defaults to 'false'. - * @type Boolean - * @default - */ -fabric.disableStyleCopyPaste = false; - -/** - * Enable webgl for filtering picture is available - * A filtering backend will be initialized, this will both take memory and - * time since a default 2048x2048 canvas will be created for the gl context - * @since 2.0.0 - * @type Boolean - * @default - */ -fabric.enableGLFiltering = true; - -/** - * Device Pixel Ratio - * @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html - */ -fabric.devicePixelRatio = fabric.window.devicePixelRatio || - fabric.window.webkitDevicePixelRatio || - fabric.window.mozDevicePixelRatio || - 1; -/** - * Browser-specific constant to adjust CanvasRenderingContext2D.shadowBlur value, - * which is unitless and not rendered equally across browsers. - * - * Values that work quite well (as of October 2017) are: - * - Chrome: 1.5 - * - Edge: 1.75 - * - Firefox: 0.9 - * - Safari: 0.95 - * - * @since 2.0.0 - * @type Number - * @default 1 - */ -fabric.browserShadowBlurConstant = 1; - -/** - * This object contains the result of arc to bezier conversion for faster retrieving if the same arc needs to be converted again. - * It was an internal variable, is accessible since version 2.3.4 - */ -fabric.arcToSegmentsCache = {}; - -/** - * This object keeps the results of the boundsOfCurve calculation mapped by the joined arguments necessary to calculate it. - * It does speed up calculation, if you parse and add always the same paths, but in case of heavy usage of freedrawing - * you do not get any speed benefit and you get a big object in memory. - * The object was a private variable before, while now is appended to the lib so that you have access to it and you - * can eventually clear it. - * It was an internal variable, is accessible since version 2.3.4 - */ -fabric.boundsOfCurveCache = {}; - -/** - * If disabled boundsOfCurveCache is not used. For apps that make heavy usage of pencil drawing probably disabling it is better - * @default true - */ -fabric.cachesBoundsOfCurve = true; - -/** - * Skip performance testing of setupGLContext and force the use of putImageData that seems to be the one that works best on - * Chrome + old hardware. if your users are experiencing empty images after filtering you may try to force this to true - * this has to be set before instantiating the filtering backend ( before filtering the first image ) - * @type Boolean - * @default false - */ -fabric.forceGLPutImageData = false; - -fabric.initFilterBackend = function () { - if (fabric.enableGLFiltering && fabric.isWebglSupported && fabric.isWebglSupported(fabric.textureSize)) { - console.log('max texture size: ' + fabric.maxTextureSize); - return (new fabric.WebglFilterBackend({ tileSize: fabric.textureSize })); - } - else if (fabric.Canvas2dFilterBackend) { - return (new fabric.Canvas2dFilterBackend()); - } -}; - - -if (typeof document !== 'undefined' && typeof window !== 'undefined') { - // ensure globality even if entire library were function wrapped (as in Meteor.js packaging system) - window.fabric = fabric; -} - - -(function () { - - /** - * @private - * @param {String} eventName - * @param {Function} handler - */ - function _removeEventListener(eventName, handler) { - if (!this.__eventListeners[eventName]) { - return; - } - var eventListener = this.__eventListeners[eventName]; - if (handler) { - eventListener[eventListener.indexOf(handler)] = false; - } - else { - fabric.util.array.fill(eventListener, false); - } - } - - /** - * Observes specified event - * @memberOf fabric.Observable - * @alias on - * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) - * @param {Function} handler Function that receives a notification when an event of the specified type occurs - * @return {Self} thisArg - * @chainable - */ - function on(eventName, handler) { - if (!this.__eventListeners) { - this.__eventListeners = {}; - } - // one object with key/value pairs was passed - if (arguments.length === 1) { - for (var prop in eventName) { - this.on(prop, eventName[prop]); - } - } - else { - if (!this.__eventListeners[eventName]) { - this.__eventListeners[eventName] = []; - } - this.__eventListeners[eventName].push(handler); - } - return this; - } - - function _once(eventName, handler) { - var _handler = function () { - handler.apply(this, arguments); - this.off(eventName, _handler); - }.bind(this); - this.on(eventName, _handler); - } - - function once(eventName, handler) { - // one object with key/value pairs was passed - if (arguments.length === 1) { - for (var prop in eventName) { - _once.call(this, prop, eventName[prop]); - } - } - else { - _once.call(this, eventName, handler); - } - return this; - } - - /** - * Stops event observing for a particular event handler. Calling this method - * without arguments removes all handlers for all events - * @memberOf fabric.Observable - * @alias off - * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) - * @param {Function} handler Function to be deleted from EventListeners - * @return {Self} thisArg - * @chainable - */ - function off(eventName, handler) { - if (!this.__eventListeners) { - return this; - } - - // remove all key/value pairs (event name -> event handler) - if (arguments.length === 0) { - for (eventName in this.__eventListeners) { - _removeEventListener.call(this, eventName); - } - } - // one object with key/value pairs was passed - else if (arguments.length === 1 && typeof arguments[0] === 'object') { - for (var prop in eventName) { - _removeEventListener.call(this, prop, eventName[prop]); - } - } - else { - _removeEventListener.call(this, eventName, handler); - } - return this; - } - - /** - * Fires event with an optional options object - * @memberOf fabric.Observable - * @param {String} eventName Event name to fire - * @param {Object} [options] Options object - * @return {Self} thisArg - * @chainable - */ - function fire(eventName, options) { - if (!this.__eventListeners) { - return this; - } - - var listenersForEvent = this.__eventListeners[eventName]; - if (!listenersForEvent) { - return this; - } - - for (var i = 0, len = listenersForEvent.length; i < len; i++) { - listenersForEvent[i] && listenersForEvent[i].call(this, options || {}); - } - this.__eventListeners[eventName] = listenersForEvent.filter(function (value) { - return value !== false; - }); - return this; - } - - /** - * @namespace fabric.Observable - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#events} - * @see {@link http://fabricjs.com/events|Events demo} - */ - fabric.Observable = { - fire: fire, - on: on, - once: once, - off: off, - }; -})(); - - -/** - * @namespace fabric.Collection - */ -fabric.Collection = { - - _objects: [], - - /** - * Adds objects to collection, Canvas or Group, then renders canvas - * (if `renderOnAddRemove` is not `false`). - * in case of Group no changes to bounding box are made. - * Objects should be instances of (or inherit from) fabric.Object - * Use of this function is highly discouraged for groups. - * you can add a bunch of objects with the add method but then you NEED - * to run a addWithUpdate call for the Group class or position/bbox will be wrong. - * @param {...fabric.Object} object Zero or more fabric instances - * @return {Self} thisArg - * @chainable - */ - add: function () { - this._objects.push.apply(this._objects, arguments); - if (this._onObjectAdded) { - for (var i = 0, length = arguments.length; i < length; i++) { - this._onObjectAdded(arguments[i]); - } - } - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) - * An object should be an instance of (or inherit from) fabric.Object - * Use of this function is highly discouraged for groups. - * you can add a bunch of objects with the insertAt method but then you NEED - * to run a addWithUpdate call for the Group class or position/bbox will be wrong. - * @param {Object} object Object to insert - * @param {Number} index Index to insert object at - * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs - * @return {Self} thisArg - * @chainable - */ - insertAt: function (object, index, nonSplicing) { - var objects = this._objects; - if (nonSplicing) { - objects[index] = object; - } - else { - objects.splice(index, 0, object); - } - this._onObjectAdded && this._onObjectAdded(object); - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) - * @param {...fabric.Object} object Zero or more fabric instances - * @return {Self} thisArg - * @chainable - */ - remove: function () { - var objects = this._objects, - index, somethingRemoved = false; - - for (var i = 0, length = arguments.length; i < length; i++) { - index = objects.indexOf(arguments[i]); - - // only call onObjectRemoved if an object was actually removed - if (index !== -1) { - somethingRemoved = true; - objects.splice(index, 1); - this._onObjectRemoved && this._onObjectRemoved(arguments[i]); - } - } - - this.renderOnAddRemove && somethingRemoved && this.requestRenderAll(); - return this; - }, - - /** - * Executes given function for each object in this group - * @param {Function} callback - * Callback invoked with current object as first argument, - * index - as second and an array of all objects - as third. - * Callback is invoked in a context of Global Object (e.g. `window`) - * when no `context` argument is given - * - * @param {Object} context Context (aka thisObject) - * @return {Self} thisArg - * @chainable - */ - forEachObject: function (callback, context) { - var objects = this.getObjects(); - for (var i = 0, len = objects.length; i < len; i++) { - callback.call(context, objects[i], i, objects); - } - return this; - }, - - /** - * Returns an array of children objects of this instance - * Type parameter introduced in 1.3.10 - * since 2.3.5 this method return always a COPY of the array; - * @param {String} [type] When specified, only objects of this type are returned - * @return {Array} - */ - getObjects: function (type) { - if (typeof type === 'undefined') { - return this._objects.concat(); - } - return this._objects.filter(function (o) { - return o.type === type; - }); - }, - - /** - * Returns object at specified index - * @param {Number} index - * @return {Self} thisArg - */ - item: function (index) { - return this._objects[index]; - }, - - /** - * Returns true if collection contains no objects - * @return {Boolean} true if collection is empty - */ - isEmpty: function () { - return this._objects.length === 0; - }, - - /** - * Returns a size of a collection (i.e: length of an array containing its objects) - * @return {Number} Collection size - */ - size: function () { - return this._objects.length; - }, - - /** - * Returns true if collection contains an object - * @param {Object} object Object to check against - * @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects` - * @return {Boolean} `true` if collection contains an object - */ - contains: function (object, deep) { - if (this._objects.indexOf(object) > -1) { - return true; - } - else if (deep) { - return this._objects.some(function (obj) { - return typeof obj.contains === 'function' && obj.contains(object, true); - }); - } - return false; - }, - - /** - * Returns number representation of a collection complexity - * @return {Number} complexity - */ - complexity: function () { - return this._objects.reduce(function (memo, current) { - memo += current.complexity ? current.complexity() : 0; - return memo; - }, 0); - } -}; - - -/** - * @namespace fabric.CommonMethods - */ -fabric.CommonMethods = { - - /** - * Sets object's properties from options - * @param {Object} [options] Options object - */ - _setOptions: function (options) { - for (var prop in options) { - this.set(prop, options[prop]); - } - }, - - /** - * @private - * @param {Object} [filler] Options object - * @param {String} [property] property to set the Gradient to - */ - _initGradient: function (filler, property) { - if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) { - this.set(property, new fabric.Gradient(filler)); - } - }, - - /** - * @private - * @param {Object} [filler] Options object - * @param {String} [property] property to set the Pattern to - * @param {Function} [callback] callback to invoke after pattern load - */ - _initPattern: function (filler, property, callback) { - if (filler && filler.source && !(filler instanceof fabric.Pattern)) { - this.set(property, new fabric.Pattern(filler, callback)); - } - else { - callback && callback(); - } - }, - - /** - * @private - */ - _setObject: function (obj) { - for (var prop in obj) { - this._set(prop, obj[prop]); - } - }, - - /** - * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. - * @param {String|Object} key Property name or object (if object, iterate over the object properties) - * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) - * @return {fabric.Object} thisArg - * @chainable - */ - set: function (key, value) { - if (typeof key === 'object') { - this._setObject(key); - } - else { - this._set(key, value); - } - return this; - }, - - _set: function (key, value) { - this[key] = value; - }, - - /** - * Toggles specified property from `true` to `false` or from `false` to `true` - * @param {String} property Property to toggle - * @return {fabric.Object} thisArg - * @chainable - */ - toggle: function (property) { - var value = this.get(property); - if (typeof value === 'boolean') { - this.set(property, !value); - } - return this; - }, - - /** - * Basic getter - * @param {String} property Property name - * @return {*} value of a property - */ - get: function (property) { - return this[property]; - } -}; - - -(function (global) { - - var sqrt = Math.sqrt, - atan2 = Math.atan2, - pow = Math.pow, - PiBy180 = Math.PI / 180, - PiBy2 = Math.PI / 2; - - /** - * @namespace fabric.util - */ - fabric.util = { - - /** - * Calculate the cos of an angle, avoiding returning floats for known results - * @static - * @memberOf fabric.util - * @param {Number} angle the angle in radians or in degree - * @return {Number} - */ - cos: function (angle) { - if (angle === 0) { return 1; } - if (angle < 0) { - // cos(a) = cos(-a) - angle = -angle; - } - var angleSlice = angle / PiBy2; - switch (angleSlice) { - case 1: case 3: return 0; - case 2: return -1; - } - return Math.cos(angle); - }, - - /** - * Calculate the sin of an angle, avoiding returning floats for known results - * @static - * @memberOf fabric.util - * @param {Number} angle the angle in radians or in degree - * @return {Number} - */ - sin: function (angle) { - if (angle === 0) { return 0; } - var angleSlice = angle / PiBy2, sign = 1; - if (angle < 0) { - // sin(-a) = -sin(a) - sign = -1; - } - switch (angleSlice) { - case 1: return sign; - case 2: return 0; - case 3: return -sign; - } - return Math.sin(angle); - }, - - /** - * Removes value from an array. - * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` - * @static - * @memberOf fabric.util - * @param {Array} array - * @param {*} value - * @return {Array} original array - */ - removeFromArray: function (array, value) { - var idx = array.indexOf(value); - if (idx !== -1) { - array.splice(idx, 1); - } - return array; - }, - - /** - * Returns random number between 2 specified ones. - * @static - * @memberOf fabric.util - * @param {Number} min lower limit - * @param {Number} max upper limit - * @return {Number} random value (between min and max) - */ - getRandomInt: function (min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; - }, - - /** - * Transforms degrees to radians. - * @static - * @memberOf fabric.util - * @param {Number} degrees value in degrees - * @return {Number} value in radians - */ - degreesToRadians: function (degrees) { - return degrees * PiBy180; - }, - - /** - * Transforms radians to degrees. - * @static - * @memberOf fabric.util - * @param {Number} radians value in radians - * @return {Number} value in degrees - */ - radiansToDegrees: function (radians) { - return radians / PiBy180; - }, - - /** - * Rotates `point` around `origin` with `radians` - * @static - * @memberOf fabric.util - * @param {fabric.Point} point The point to rotate - * @param {fabric.Point} origin The origin of the rotation - * @param {Number} radians The radians of the angle for the rotation - * @return {fabric.Point} The new rotated point - */ - rotatePoint: function (point, origin, radians) { - var newPoint = new fabric.Point(point.x - origin.x, point.y - origin.y), - v = fabric.util.rotateVector(newPoint, radians); - return new fabric.Point(v.x, v.y).addEquals(origin); - }, - - /** - * Rotates `vector` with `radians` - * @static - * @memberOf fabric.util - * @param {Object} vector The vector to rotate (x and y) - * @param {Number} radians The radians of the angle for the rotation - * @return {Object} The new rotated point - */ - rotateVector: function (vector, radians) { - var sin = fabric.util.sin(radians), - cos = fabric.util.cos(radians), - rx = vector.x * cos - vector.y * sin, - ry = vector.x * sin + vector.y * cos; - return { - x: rx, - y: ry - }; - }, - - /** - * Creates a vetor from points represented as a point - * @static - * @memberOf fabric.util - * - * @typedef {Object} Point - * @property {number} x - * @property {number} y - * - * @param {Point} from - * @param {Point} to - * @returns {Point} vector - */ - createVector: function (from, to) { - return new fabric.Point(to.x - from.x, to.y - from.y); - }, - - /** - * Calculates angle between 2 vectors using dot product - * @static - * @memberOf fabric.util - * @param {Point} a - * @param {Point} b - * @returns the angle in radian between the vectors - */ - calcAngleBetweenVectors: function (a, b) { - return Math.acos((a.x * b.x + a.y * b.y) / (Math.hypot(a.x, a.y) * Math.hypot(b.x, b.y))); - }, - - /** - * @static - * @memberOf fabric.util - * @param {Point} v - * @returns {Point} vector representing the unit vector of pointing to the direction of `v` - */ - getHatVector: function (v) { - return new fabric.Point(v.x, v.y).multiply(1 / Math.hypot(v.x, v.y)); - }, - - /** - * @static - * @memberOf fabric.util - * @param {Point} A - * @param {Point} B - * @param {Point} C - * @returns {{ vector: Point, angle: number }} vector representing the bisector of A and A's angle - */ - getBisector: function (A, B, C) { - var AB = fabric.util.createVector(A, B), AC = fabric.util.createVector(A, C); - var alpha = fabric.util.calcAngleBetweenVectors(AB, AC); - // check if alpha is relative to AB->BC - var ro = fabric.util.calcAngleBetweenVectors(fabric.util.rotateVector(AB, alpha), AC); - var phi = alpha * (ro === 0 ? 1 : -1) / 2; - return { - vector: fabric.util.getHatVector(fabric.util.rotateVector(AB, phi)), - angle: alpha - }; - }, - - /** - * Project stroke width on points returning 2 projections for each point as follows: - * - `miter`: 2 points corresponding to the outer boundary and the inner boundary of stroke. - * - `bevel`: 2 points corresponding to the bevel boundaries, tangent to the bisector. - * - `round`: same as `bevel` - * Used to calculate object's bounding box - * @static - * @memberOf fabric.util - * @param {Point[]} points - * @param {Object} options - * @param {number} options.strokeWidth - * @param {'miter'|'bevel'|'round'} options.strokeLineJoin - * @param {number} options.strokeMiterLimit https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit - * @param {boolean} options.strokeUniform - * @param {number} options.scaleX - * @param {number} options.scaleY - * @param {boolean} [openPath] whether the shape is open or not, affects the calculations of the first and last points - * @returns {fabric.Point[]} array of size 2n/4n of all suspected points - */ - projectStrokeOnPoints: function (points, options, openPath) { - var coords = [], s = options.strokeWidth / 2, - strokeUniformScalar = options.strokeUniform ? - new fabric.Point(1 / options.scaleX, 1 / options.scaleY) : new fabric.Point(1, 1), - getStrokeHatVector = function (v) { - var scalar = s / (Math.hypot(v.x, v.y)); - return new fabric.Point(v.x * scalar * strokeUniformScalar.x, v.y * scalar * strokeUniformScalar.y); - }; - if (points.length <= 1) { return coords; } - points.forEach(function (p, index) { - var A = new fabric.Point(p.x, p.y), B, C; - if (index === 0) { - C = points[index + 1]; - B = openPath ? getStrokeHatVector(fabric.util.createVector(C, A)).addEquals(A) : points[points.length - 1]; - } - else if (index === points.length - 1) { - B = points[index - 1]; - C = openPath ? getStrokeHatVector(fabric.util.createVector(B, A)).addEquals(A) : points[0]; - } - else { - B = points[index - 1]; - C = points[index + 1]; - } - var bisector = fabric.util.getBisector(A, B, C), - bisectorVector = bisector.vector, - alpha = bisector.angle, - scalar, - miterVector; - if (options.strokeLineJoin === 'miter') { - scalar = -s / Math.sin(alpha / 2); - miterVector = new fabric.Point( - bisectorVector.x * scalar * strokeUniformScalar.x, - bisectorVector.y * scalar * strokeUniformScalar.y - ); - if (Math.hypot(miterVector.x, miterVector.y) / s <= options.strokeMiterLimit) { - coords.push(A.add(miterVector)); - coords.push(A.subtract(miterVector)); - return; - } - } - scalar = -s * Math.SQRT2; - miterVector = new fabric.Point( - bisectorVector.x * scalar * strokeUniformScalar.x, - bisectorVector.y * scalar * strokeUniformScalar.y - ); - coords.push(A.add(miterVector)); - coords.push(A.subtract(miterVector)); - }); - return coords; - }, - - /** - * Apply transform t to point p - * @static - * @memberOf fabric.util - * @param {fabric.Point} p The point to transform - * @param {Array} t The transform - * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied - * @return {fabric.Point} The transformed point - */ - transformPoint: function (p, t, ignoreOffset) { - if (ignoreOffset) { - return new fabric.Point( - t[0] * p.x + t[2] * p.y, - t[1] * p.x + t[3] * p.y - ); - } - return new fabric.Point( - t[0] * p.x + t[2] * p.y + t[4], - t[1] * p.x + t[3] * p.y + t[5] - ); - }, - - /** - * Returns coordinates of points's bounding rectangle (left, top, width, height) - * @param {Array} points 4 points array - * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix - * @return {Object} Object with left, top, width, height properties - */ - makeBoundingBoxFromPoints: function (points, transform) { - if (transform) { - for (var i = 0; i < points.length; i++) { - points[i] = fabric.util.transformPoint(points[i], transform); - } - } - var xPoints = [points[0].x, points[1].x, points[2].x, points[3].x], - minX = fabric.util.array.min(xPoints), - maxX = fabric.util.array.max(xPoints), - width = maxX - minX, - yPoints = [points[0].y, points[1].y, points[2].y, points[3].y], - minY = fabric.util.array.min(yPoints), - maxY = fabric.util.array.max(yPoints), - height = maxY - minY; - - return { - left: minX, - top: minY, - width: width, - height: height - }; - }, - - /** - * Invert transformation t - * @static - * @memberOf fabric.util - * @param {Array} t The transform - * @return {Array} The inverted transform - */ - invertTransform: function (t) { - var a = 1 / (t[0] * t[3] - t[1] * t[2]), - r = [a * t[3], -a * t[1], -a * t[2], a * t[0]], - o = fabric.util.transformPoint({ x: t[4], y: t[5] }, r, true); - r[4] = -o.x; - r[5] = -o.y; - return r; - }, - - /** - * A wrapper around Number#toFixed, which contrary to native method returns number, not string. - * @static - * @memberOf fabric.util - * @param {Number|String} number number to operate on - * @param {Number} fractionDigits number of fraction digits to "leave" - * @return {Number} - */ - toFixed: function (number, fractionDigits) { - return parseFloat(Number(number).toFixed(fractionDigits)); - }, - - /** - * Converts from attribute value to pixel value if applicable. - * Returns converted pixels or original value not converted. - * @param {Number|String} value number to operate on - * @param {Number} fontSize - * @return {Number|String} - */ - parseUnit: function (value, fontSize) { - var unit = /\D{0,2}$/.exec(value), - number = parseFloat(value); - if (!fontSize) { - fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; - } - switch (unit[0]) { - case 'mm': - return number * fabric.DPI / 25.4; - - case 'cm': - return number * fabric.DPI / 2.54; - - case 'in': - return number * fabric.DPI; - - case 'pt': - return number * fabric.DPI / 72; // or * 4 / 3 - - case 'pc': - return number * fabric.DPI / 72 * 12; // or * 16 - - case 'em': - return number * fontSize; - - default: - return number; - } - }, - - /** - * Function which always returns `false`. - * @static - * @memberOf fabric.util - * @return {Boolean} - */ - falseFunction: function () { - return false; - }, - - /** - * Returns klass "Class" object of given namespace - * @memberOf fabric.util - * @param {String} type Type of object (eg. 'circle') - * @param {String} namespace Namespace to get klass "Class" object from - * @return {Object} klass "Class" - */ - getKlass: function (type, namespace) { - // capitalize first letter only - type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); - return fabric.util.resolveNamespace(namespace)[type]; - }, - - /** - * Returns array of attributes for given svg that fabric parses - * @memberOf fabric.util - * @param {String} type Type of svg element (eg. 'circle') - * @return {Array} string names of supported attributes - */ - getSvgAttributes: function (type) { - var attributes = [ - 'instantiated_by_use', - 'style', - 'id', - 'class' - ]; - switch (type) { - case 'linearGradient': - attributes = attributes.concat(['x1', 'y1', 'x2', 'y2', 'gradientUnits', 'gradientTransform']); - break; - case 'radialGradient': - attributes = attributes.concat(['gradientUnits', 'gradientTransform', 'cx', 'cy', 'r', 'fx', 'fy', 'fr']); - break; - case 'stop': - attributes = attributes.concat(['offset', 'stop-color', 'stop-opacity']); - break; - } - return attributes; - }, - - /** - * Returns object of given namespace - * @memberOf fabric.util - * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' - * @return {Object} Object for given namespace (default fabric) - */ - resolveNamespace: function (namespace) { - if (!namespace) { - return fabric; - } - - var parts = namespace.split('.'), - len = parts.length, i, - obj = global || fabric.window; - - for (i = 0; i < len; ++i) { - obj = obj[parts[i]]; - } - - return obj; - }, - - /** - * Loads image element from given url and passes it to a callback - * @memberOf fabric.util - * @param {String} url URL representing an image - * @param {Function} callback Callback; invoked with loaded image - * @param {*} [context] Context to invoke callback in - * @param {Object} [crossOrigin] crossOrigin value to set image element to - */ - loadImage: function (url, callback, context, crossOrigin) { - if (!url) { - callback && callback.call(context, url); - return; - } - - var img = fabric.util.createImage(); - - /** @ignore */ - var onLoadCallback = function () { - callback && callback.call(context, img, false); - img = img.onload = img.onerror = null; - }; - - img.onload = onLoadCallback; - /** @ignore */ - img.onerror = function () { - fabric.log('Error loading ' + img.src); - callback && callback.call(context, null, true); - img = img.onload = img.onerror = null; - }; - - // data-urls appear to be buggy with crossOrigin - // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 - // see https://code.google.com/p/chromium/issues/detail?id=315152 - // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 - // crossOrigin null is the same as not set. - if (url.indexOf('data') !== 0 && - crossOrigin !== undefined && - crossOrigin !== null) { - img.crossOrigin = crossOrigin; - } - - // IE10 / IE11-Fix: SVG contents from data: URI - // will only be available if the IMG is present - // in the DOM (and visible) - if (url.substring(0, 14) === 'data:image/svg') { - img.onload = null; - fabric.util.loadImageInDom(img, onLoadCallback); - } - - img.src = url; - }, - - /** - * Attaches SVG image with data: URL to the dom - * @memberOf fabric.util - * @param {Object} img Image object with data:image/svg src - * @param {Function} callback Callback; invoked with loaded image - * @return {Object} DOM element (div containing the SVG image) - */ - loadImageInDom: function (img, onLoadCallback) { - var div = fabric.document.createElement('div'); - div.style.width = div.style.height = '1px'; - div.style.left = div.style.top = '-100%'; - div.style.position = 'absolute'; - div.appendChild(img); - fabric.document.querySelector('body').appendChild(div); - /** - * Wrap in function to: - * 1. Call existing callback - * 2. Cleanup DOM - */ - img.onload = function () { - onLoadCallback(); - div.parentNode.removeChild(div); - div = null; - }; - }, - - /** - * Creates corresponding fabric instances from their object representations - * @static - * @memberOf fabric.util - * @param {Array} objects Objects to enliven - * @param {Function} callback Callback to invoke when all objects are created - * @param {String} namespace Namespace to get klass "Class" object from - * @param {Function} reviver Method for further parsing of object elements, - * called after each fabric object created. - */ - enlivenObjects: function (objects, callback, namespace, reviver) { - objects = objects || []; - - var enlivenedObjects = [], - numLoadedObjects = 0, - numTotalObjects = objects.length; - - function onLoaded() { - if (++numLoadedObjects === numTotalObjects) { - callback && callback(enlivenedObjects.filter(function (obj) { - // filter out undefined objects (objects that gave error) - return obj; - })); - } - } - - if (!numTotalObjects) { - callback && callback(enlivenedObjects); - return; - } - - objects.forEach(function (o, index) { - // if sparse array - if (!o || !o.type) { - onLoaded(); - return; - } - var klass = fabric.util.getKlass(o.type, namespace); - klass.fromObject(o, function (obj, error) { - error || (enlivenedObjects[index] = obj); - reviver && reviver(o, obj, error); - onLoaded(); - }); - }); - }, - - /** - * Creates corresponding fabric instances residing in an object, e.g. `clipPath` - * @see {@link fabric.Object.ENLIVEN_PROPS} - * @param {Object} object - * @param {Object} [context] assign enlived props to this object (pass null to skip this) - * @param {(objects:fabric.Object[]) => void} callback - */ - enlivenObjectEnlivables: function (object, context, callback) { - var enlivenProps = fabric.Object.ENLIVEN_PROPS.filter(function (key) { return !!object[key]; }); - fabric.util.enlivenObjects(enlivenProps.map(function (key) { return object[key]; }), function (enlivedProps) { - var objects = {}; - enlivenProps.forEach(function (key, index) { - objects[key] = enlivedProps[index]; - context && (context[key] = enlivedProps[index]); - }); - callback && callback(objects); - }); - }, - - /** - * Create and wait for loading of patterns - * @static - * @memberOf fabric.util - * @param {Array} patterns Objects to enliven - * @param {Function} callback Callback to invoke when all objects are created - * called after each fabric object created. - */ - enlivenPatterns: function (patterns, callback) { - patterns = patterns || []; - - function onLoaded() { - if (++numLoadedPatterns === numPatterns) { - callback && callback(enlivenedPatterns); - } - } - - var enlivenedPatterns = [], - numLoadedPatterns = 0, - numPatterns = patterns.length; - - if (!numPatterns) { - callback && callback(enlivenedPatterns); - return; - } - - patterns.forEach(function (p, index) { - if (p && p.source) { - new fabric.Pattern(p, function (pattern) { - enlivenedPatterns[index] = pattern; - onLoaded(); - }); - } - else { - enlivenedPatterns[index] = p; - onLoaded(); - } - }); - }, - - /** - * Groups SVG elements (usually those retrieved from SVG document) - * @static - * @memberOf fabric.util - * @param {Array} elements SVG elements to group - * @param {Object} [options] Options object - * @param {String} path Value to set sourcePath to - * @return {fabric.Object|fabric.Group} - */ - groupSVGElements: function (elements, options, path) { - var object; - if (elements && elements.length === 1) { - if (typeof path !== 'undefined') { - elements[0].sourcePath = path; - } - return elements[0]; - } - if (options) { - if (options.width && options.height) { - options.centerPoint = { - x: options.width / 2, - y: options.height / 2 - }; - } - else { - delete options.width; - delete options.height; - } - } - object = new fabric.Group(elements, options); - if (typeof path !== 'undefined') { - object.sourcePath = path; - } - return object; - }, - - /** - * Populates an object with properties of another object - * @static - * @memberOf fabric.util - * @param {Object} source Source object - * @param {Object} destination Destination object - * @return {Array} properties Properties names to include - */ - populateWithProperties: function (source, destination, properties) { - if (properties && Array.isArray(properties)) { - for (var i = 0, len = properties.length; i < len; i++) { - if (properties[i] in source) { - destination[properties[i]] = source[properties[i]]; - } - } - } - }, - - /** - * Creates canvas element - * @static - * @memberOf fabric.util - * @return {CanvasElement} initialized canvas element - */ - createCanvasElement: function () { - return fabric.document.createElement('canvas'); - }, - - /** - * Creates a canvas element that is a copy of another and is also painted - * @param {CanvasElement} canvas to copy size and content of - * @static - * @memberOf fabric.util - * @return {CanvasElement} initialized canvas element - */ - copyCanvasElement: function (canvas) { - var newCanvas = fabric.util.createCanvasElement(); - newCanvas.width = canvas.width; - newCanvas.height = canvas.height; - newCanvas.getContext('2d').drawImage(canvas, 0, 0); - return newCanvas; - }, - - /** - * since 2.6.0 moved from canvas instance to utility. - * @param {CanvasElement} canvasEl to copy size and content of - * @param {String} format 'jpeg' or 'png', in some browsers 'webp' is ok too - * @param {Number} quality <= 1 and > 0 - * @static - * @memberOf fabric.util - * @return {String} data url - */ - toDataURL: function (canvasEl, format, quality) { - return canvasEl.toDataURL('image/' + format, quality); - }, - - /** - * Creates image element (works on client and node) - * @static - * @memberOf fabric.util - * @return {HTMLImageElement} HTML image element - */ - createImage: function () { - return fabric.document.createElement('img'); - }, - - /** - * Multiply matrix A by matrix B to nest transformations - * @static - * @memberOf fabric.util - * @param {Array} a First transformMatrix - * @param {Array} b Second transformMatrix - * @param {Boolean} is2x2 flag to multiply matrices as 2x2 matrices - * @return {Array} The product of the two transform matrices - */ - multiplyTransformMatrices: function (a, b, is2x2) { - // Matrix multiply a * b - return [ - a[0] * b[0] + a[2] * b[1], - a[1] * b[0] + a[3] * b[1], - a[0] * b[2] + a[2] * b[3], - a[1] * b[2] + a[3] * b[3], - is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4], - is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5] - ]; - }, - - /** - * Decomposes standard 2x3 matrix into transform components - * @static - * @memberOf fabric.util - * @param {Array} a transformMatrix - * @return {Object} Components of transform - */ - qrDecompose: function (a) { - var angle = atan2(a[1], a[0]), - denom = pow(a[0], 2) + pow(a[1], 2), - scaleX = sqrt(denom), - scaleY = (a[0] * a[3] - a[2] * a[1]) / scaleX, - skewX = atan2(a[0] * a[2] + a[1] * a[3], denom); - return { - angle: angle / PiBy180, - scaleX: scaleX, - scaleY: scaleY, - skewX: skewX / PiBy180, - skewY: 0, - translateX: a[4], - translateY: a[5] - }; - }, - - /** - * Returns a transform matrix starting from an object of the same kind of - * the one returned from qrDecompose, useful also if you want to calculate some - * transformations from an object that is not enlived yet - * @static - * @memberOf fabric.util - * @param {Object} options - * @param {Number} [options.angle] angle in degrees - * @return {Number[]} transform matrix - */ - calcRotateMatrix: function (options) { - if (!options.angle) { - return fabric.iMatrix.concat(); - } - var theta = fabric.util.degreesToRadians(options.angle), - cos = fabric.util.cos(theta), - sin = fabric.util.sin(theta); - return [cos, sin, -sin, cos, 0, 0]; - }, - - /** - * Returns a transform matrix starting from an object of the same kind of - * the one returned from qrDecompose, useful also if you want to calculate some - * transformations from an object that is not enlived yet. - * is called DimensionsTransformMatrix because those properties are the one that influence - * the size of the resulting box of the object. - * @static - * @memberOf fabric.util - * @param {Object} options - * @param {Number} [options.scaleX] - * @param {Number} [options.scaleY] - * @param {Boolean} [options.flipX] - * @param {Boolean} [options.flipY] - * @param {Number} [options.skewX] - * @param {Number} [options.skewY] - * @return {Number[]} transform matrix - */ - calcDimensionsMatrix: function (options) { - var scaleX = typeof options.scaleX === 'undefined' ? 1 : options.scaleX, - scaleY = typeof options.scaleY === 'undefined' ? 1 : options.scaleY, - scaleMatrix = [ - options.flipX ? -scaleX : scaleX, - 0, - 0, - options.flipY ? -scaleY : scaleY, - 0, - 0], - multiply = fabric.util.multiplyTransformMatrices, - degreesToRadians = fabric.util.degreesToRadians; - if (options.skewX) { - scaleMatrix = multiply( - scaleMatrix, - [1, 0, Math.tan(degreesToRadians(options.skewX)), 1], - true); - } - if (options.skewY) { - scaleMatrix = multiply( - scaleMatrix, - [1, Math.tan(degreesToRadians(options.skewY)), 0, 1], - true); - } - return scaleMatrix; - }, - - /** - * Returns a transform matrix starting from an object of the same kind of - * the one returned from qrDecompose, useful also if you want to calculate some - * transformations from an object that is not enlived yet - * @static - * @memberOf fabric.util - * @param {Object} options - * @param {Number} [options.angle] - * @param {Number} [options.scaleX] - * @param {Number} [options.scaleY] - * @param {Boolean} [options.flipX] - * @param {Boolean} [options.flipY] - * @param {Number} [options.skewX] - * @param {Number} [options.skewX] - * @param {Number} [options.translateX] - * @param {Number} [options.translateY] - * @return {Number[]} transform matrix - */ - composeMatrix: function (options) { - var matrix = [1, 0, 0, 1, options.translateX || 0, options.translateY || 0], - multiply = fabric.util.multiplyTransformMatrices; - if (options.angle) { - matrix = multiply(matrix, fabric.util.calcRotateMatrix(options)); - } - if (options.scaleX !== 1 || options.scaleY !== 1 || - options.skewX || options.skewY || options.flipX || options.flipY) { - matrix = multiply(matrix, fabric.util.calcDimensionsMatrix(options)); - } - return matrix; - }, - - /** - * reset an object transform state to neutral. Top and left are not accounted for - * @static - * @memberOf fabric.util - * @param {fabric.Object} target object to transform - */ - resetObjectTransform: function (target) { - target.scaleX = 1; - target.scaleY = 1; - target.skewX = 0; - target.skewY = 0; - target.flipX = false; - target.flipY = false; - target.rotate(0); - }, - - /** - * Extract Object transform values - * @static - * @memberOf fabric.util - * @param {fabric.Object} target object to read from - * @return {Object} Components of transform - */ - saveObjectTransform: function (target) { - return { - scaleX: target.scaleX, - scaleY: target.scaleY, - skewX: target.skewX, - skewY: target.skewY, - angle: target.angle, - left: target.left, - flipX: target.flipX, - flipY: target.flipY, - top: target.top - }; - }, - - /** - * Returns true if context has transparent pixel - * at specified location (taking tolerance into account) - * @param {CanvasRenderingContext2D} ctx context - * @param {Number} x x coordinate - * @param {Number} y y coordinate - * @param {Number} tolerance Tolerance - */ - isTransparent: function (ctx, x, y, tolerance) { - - // If tolerance is > 0 adjust start coords to take into account. - // If moves off Canvas fix to 0 - if (tolerance > 0) { - if (x > tolerance) { - x -= tolerance; - } - else { - x = 0; - } - if (y > tolerance) { - y -= tolerance; - } - else { - y = 0; - } - } - - var _isTransparent = true, i, temp, - imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1), - l = imageData.data.length; - - // Split image data - for tolerance > 1, pixelDataSize = 4; - for (i = 3; i < l; i += 4) { - temp = imageData.data[i]; - _isTransparent = temp <= 0; - if (_isTransparent === false) { - break; // Stop if colour found - } - } - - imageData = null; - - return _isTransparent; - }, - - /** - * Parse preserveAspectRatio attribute from element - * @param {string} attribute to be parsed - * @return {Object} an object containing align and meetOrSlice attribute - */ - parsePreserveAspectRatioAttribute: function (attribute) { - var meetOrSlice = 'meet', alignX = 'Mid', alignY = 'Mid', - aspectRatioAttrs = attribute.split(' '), align; - - if (aspectRatioAttrs && aspectRatioAttrs.length) { - meetOrSlice = aspectRatioAttrs.pop(); - if (meetOrSlice !== 'meet' && meetOrSlice !== 'slice') { - align = meetOrSlice; - meetOrSlice = 'meet'; - } - else if (aspectRatioAttrs.length) { - align = aspectRatioAttrs.pop(); - } - } - //divide align in alignX and alignY - alignX = align !== 'none' ? align.slice(1, 4) : 'none'; - alignY = align !== 'none' ? align.slice(5, 8) : 'none'; - return { - meetOrSlice: meetOrSlice, - alignX: alignX, - alignY: alignY - }; - }, - - /** - * Clear char widths cache for the given font family or all the cache if no - * fontFamily is specified. - * Use it if you know you are loading fonts in a lazy way and you are not waiting - * for custom fonts to load properly when adding text objects to the canvas. - * If a text object is added when its own font is not loaded yet, you will get wrong - * measurement and so wrong bounding boxes. - * After the font cache is cleared, either change the textObject text content or call - * initDimensions() to trigger a recalculation - * @memberOf fabric.util - * @param {String} [fontFamily] font family to clear - */ - clearFabricFontCache: function (fontFamily) { - fontFamily = (fontFamily || '').toLowerCase(); - if (!fontFamily) { - fabric.charWidthsCache = {}; - } - else if (fabric.charWidthsCache[fontFamily]) { - delete fabric.charWidthsCache[fontFamily]; - } - }, - - /** - * Given current aspect ratio, determines the max width and height that can - * respect the total allowed area for the cache. - * @memberOf fabric.util - * @param {Number} ar aspect ratio - * @param {Number} maximumArea Maximum area you want to achieve - * @return {Object.x} Limited dimensions by X - * @return {Object.y} Limited dimensions by Y - */ - limitDimsByArea: function (ar, maximumArea) { - var roughWidth = Math.sqrt(maximumArea * ar), - perfLimitSizeY = Math.floor(maximumArea / roughWidth); - return { x: Math.floor(roughWidth), y: perfLimitSizeY }; - }, - - capValue: function (min, value, max) { - return Math.max(min, Math.min(value, max)); - }, - - /** - * Finds the scale for the object source to fit inside the object destination, - * keeping aspect ratio intact. - * respect the total allowed area for the cache. - * @memberOf fabric.util - * @param {Object | fabric.Object} source - * @param {Number} source.height natural unscaled height of the object - * @param {Number} source.width natural unscaled width of the object - * @param {Object | fabric.Object} destination - * @param {Number} destination.height natural unscaled height of the object - * @param {Number} destination.width natural unscaled width of the object - * @return {Number} scale factor to apply to source to fit into destination - */ - findScaleToFit: function (source, destination) { - return Math.min(destination.width / source.width, destination.height / source.height); - }, - - /** - * Finds the scale for the object source to cover entirely the object destination, - * keeping aspect ratio intact. - * respect the total allowed area for the cache. - * @memberOf fabric.util - * @param {Object | fabric.Object} source - * @param {Number} source.height natural unscaled height of the object - * @param {Number} source.width natural unscaled width of the object - * @param {Object | fabric.Object} destination - * @param {Number} destination.height natural unscaled height of the object - * @param {Number} destination.width natural unscaled width of the object - * @return {Number} scale factor to apply to source to cover destination - */ - findScaleToCover: function (source, destination) { - return Math.max(destination.width / source.width, destination.height / source.height); - }, - - /** - * given an array of 6 number returns something like `"matrix(...numbers)"` - * @memberOf fabric.util - * @param {Array} transform an array with 6 numbers - * @return {String} transform matrix for svg - * @return {Object.y} Limited dimensions by Y - */ - matrixToSVG: function (transform) { - return 'matrix(' + transform.map(function (value) { - return fabric.util.toFixed(value, fabric.Object.NUM_FRACTION_DIGITS); - }).join(' ') + ')'; - }, - - /** - * given an object and a transform, apply the inverse transform to the object, - * this is equivalent to remove from that object that transformation, so that - * added in a space with the removed transform, the object will be the same as before. - * Removing from an object a transform that scale by 2 is like scaling it by 1/2. - * Removing from an object a transfrom that rotate by 30deg is like rotating by 30deg - * in the opposite direction. - * This util is used to add objects inside transformed groups or nested groups. - * @memberOf fabric.util - * @param {fabric.Object} object the object you want to transform - * @param {Array} transform the destination transform - */ - removeTransformFromObject: function (object, transform) { - var inverted = fabric.util.invertTransform(transform), - finalTransform = fabric.util.multiplyTransformMatrices(inverted, object.calcOwnMatrix()); - fabric.util.applyTransformToObject(object, finalTransform); - }, - - /** - * given an object and a transform, apply the transform to the object. - * this is equivalent to change the space where the object is drawn. - * Adding to an object a transform that scale by 2 is like scaling it by 2. - * This is used when removing an object from an active selection for example. - * @memberOf fabric.util - * @param {fabric.Object} object the object you want to transform - * @param {Array} transform the destination transform - */ - addTransformToObject: function (object, transform) { - fabric.util.applyTransformToObject( - object, - fabric.util.multiplyTransformMatrices(transform, object.calcOwnMatrix()) - ); - }, - - /** - * discard an object transform state and apply the one from the matrix. - * @memberOf fabric.util - * @param {fabric.Object} object the object you want to transform - * @param {Array} transform the destination transform - */ - applyTransformToObject: function (object, transform) { - var options = fabric.util.qrDecompose(transform), - center = new fabric.Point(options.translateX, options.translateY); - object.flipX = false; - object.flipY = false; - object.set('scaleX', options.scaleX); - object.set('scaleY', options.scaleY); - object.skewX = options.skewX; - object.skewY = options.skewY; - object.angle = options.angle; - object.setPositionByOrigin(center, 'center', 'center'); - }, - - /** - * given a width and height, return the size of the bounding box - * that can contains the box with width/height with applied transform - * described in options. - * Use to calculate the boxes around objects for controls. - * @memberOf fabric.util - * @param {Number} width - * @param {Number} height - * @param {Object} options - * @param {Number} options.scaleX - * @param {Number} options.scaleY - * @param {Number} options.skewX - * @param {Number} options.skewY - * @return {Object.x} width of containing - * @return {Object.y} height of containing - */ - sizeAfterTransform: function (width, height, options) { - var dimX = width / 2, dimY = height / 2, - points = [ - { - x: -dimX, - y: -dimY - }, - { - x: dimX, - y: -dimY - }, - { - x: -dimX, - y: dimY - }, - { - x: dimX, - y: dimY - }], - transformMatrix = fabric.util.calcDimensionsMatrix(options), - bbox = fabric.util.makeBoundingBoxFromPoints(points, transformMatrix); - return { - x: bbox.width, - y: bbox.height, - }; - }, - - /** - * Merges 2 clip paths into one visually equal clip path - * - * **IMPORTANT**:\ - * Does **NOT** clone the arguments, clone them proir if necessary. - * - * Creates a wrapper (group) that contains one clip path and is clipped by the other so content is kept where both overlap. - * Use this method if both the clip paths may have nested clip paths of their own, so assigning one to the other's clip path property is not possible. - * - * In order to handle the `inverted` property we follow logic described in the following cases:\ - * **(1)** both clip paths are inverted - the clip paths pass the inverted prop to the wrapper and loose it themselves.\ - * **(2)** one is inverted and the other isn't - the wrapper shouldn't become inverted and the inverted clip path must clip the non inverted one to produce an identical visual effect.\ - * **(3)** both clip paths are not inverted - wrapper and clip paths remain unchanged. - * - * @memberOf fabric.util - * @param {fabric.Object} c1 - * @param {fabric.Object} c2 - * @returns {fabric.Object} merged clip path - */ - mergeClipPaths: function (c1, c2) { - var a = c1, b = c2; - if (a.inverted && !b.inverted) { - // case (2) - a = c2; - b = c1; - } - // `b` becomes `a`'s clip path so we transform `b` to `a` coordinate plane - fabric.util.applyTransformToObject( - b, - fabric.util.multiplyTransformMatrices( - fabric.util.invertTransform(a.calcTransformMatrix()), - b.calcTransformMatrix() - ) - ); - // assign the `inverted` prop to the wrapping group - var inverted = a.inverted && b.inverted; - if (inverted) { - // case (1) - a.inverted = b.inverted = false; - } - return new fabric.Group([a], { clipPath: b, inverted: inverted }); - }, - - /** - * @memberOf fabric.util - * @param {Object} prevStyle first style to compare - * @param {Object} thisStyle second style to compare - * @param {boolean} forTextSpans whether to check overline, underline, and line-through properties - * @return {boolean} true if the style changed - */ - hasStyleChanged: function (prevStyle, thisStyle, forTextSpans) { - forTextSpans = forTextSpans || false; - return (prevStyle.fill !== thisStyle.fill || - prevStyle.stroke !== thisStyle.stroke || - prevStyle.strokeWidth !== thisStyle.strokeWidth || - prevStyle.fontSize !== thisStyle.fontSize || - prevStyle.fontFamily !== thisStyle.fontFamily || - prevStyle.fontWeight !== thisStyle.fontWeight || - prevStyle.fontStyle !== thisStyle.fontStyle || - prevStyle.textBackgroundColor !== thisStyle.textBackgroundColor || - prevStyle.deltaY !== thisStyle.deltaY) || - (forTextSpans && - (prevStyle.overline !== thisStyle.overline || - prevStyle.underline !== thisStyle.underline || - prevStyle.linethrough !== thisStyle.linethrough)); - }, - - /** - * Returns the array form of a text object's inline styles property with styles grouped in ranges - * rather than per character. This format is less verbose, and is better suited for storage - * so it is used in serialization (not during runtime). - * @memberOf fabric.util - * @param {object} styles per character styles for a text object - * @param {String} text the text string that the styles are applied to - * @return {{start: number, end: number, style: object}[]} - */ - stylesToArray: function (styles, text) { - // clone style structure to prevent mutation - var styles = fabric.util.object.clone(styles, true), - textLines = text.split('\n'), - charIndex = -1, prevStyle = {}, stylesArray = []; - //loop through each textLine - for (var i = 0; i < textLines.length; i++) { - if (!styles[i]) { - //no styles exist for this line, so add the line's length to the charIndex total - charIndex += textLines[i].length; - continue; - } - //loop through each character of the current line - for (var c = 0; c < textLines[i].length; c++) { - charIndex++; - var thisStyle = styles[i][c]; - //check if style exists for this character - if (thisStyle && Object.keys(thisStyle).length > 0) { - var styleChanged = fabric.util.hasStyleChanged(prevStyle, thisStyle, true); - if (styleChanged) { - stylesArray.push({ - start: charIndex, - end: charIndex + 1, - style: thisStyle - }); - } - else { - //if style is the same as previous character, increase end index - stylesArray[stylesArray.length - 1].end++; - } - } - prevStyle = thisStyle || {}; - } - } - return stylesArray; - }, - - /** - * Returns the object form of the styles property with styles that are assigned per - * character rather than grouped by range. This format is more verbose, and is - * only used during runtime (not for serialization/storage) - * @memberOf fabric.util - * @param {Array} styles the serialized form of a text object's styles - * @param {String} text the text string that the styles are applied to - * @return {Object} - */ - stylesFromArray: function (styles, text) { - if (!Array.isArray(styles)) { - return styles; - } - var textLines = text.split('\n'), - charIndex = -1, styleIndex = 0, stylesObject = {}; - //loop through each textLine - for (var i = 0; i < textLines.length; i++) { - //loop through each character of the current line - for (var c = 0; c < textLines[i].length; c++) { - charIndex++; - //check if there's a style collection that includes the current character - if (styles[styleIndex] - && styles[styleIndex].start <= charIndex - && charIndex < styles[styleIndex].end) { - //create object for line index if it doesn't exist - stylesObject[i] = stylesObject[i] || {}; - //assign a style at this character's index - stylesObject[i][c] = Object.assign({}, styles[styleIndex].style); - //if character is at the end of the current style collection, move to the next - if (charIndex === styles[styleIndex].end - 1) { - styleIndex++; - } - } - } - } - return stylesObject; - } - }; -})(typeof exports !== 'undefined' ? exports : this); - - -(function () { - var _join = Array.prototype.join, - commandLengths = { - m: 2, - l: 2, - h: 1, - v: 1, - c: 6, - s: 4, - q: 4, - t: 2, - a: 7 - }, - repeatedCommands = { - m: 'l', - M: 'L' - }; - function segmentToBezier(th2, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY) { - var costh2 = fabric.util.cos(th2), - sinth2 = fabric.util.sin(th2), - costh3 = fabric.util.cos(th3), - sinth3 = fabric.util.sin(th3), - toX = cosTh * rx * costh3 - sinTh * ry * sinth3 + cx1, - toY = sinTh * rx * costh3 + cosTh * ry * sinth3 + cy1, - cp1X = fromX + mT * (-cosTh * rx * sinth2 - sinTh * ry * costh2), - cp1Y = fromY + mT * (-sinTh * rx * sinth2 + cosTh * ry * costh2), - cp2X = toX + mT * (cosTh * rx * sinth3 + sinTh * ry * costh3), - cp2Y = toY + mT * (sinTh * rx * sinth3 - cosTh * ry * costh3); - - return ['C', - cp1X, cp1Y, - cp2X, cp2Y, - toX, toY - ]; - } - - /* Adapted from http://dxr.mozilla.org/mozilla-central/source/content/svg/content/src/nsSVGPathDataParser.cpp - * by Andrea Bogazzi code is under MPL. if you don't have a copy of the license you can take it here - * http://mozilla.org/MPL/2.0/ - */ - function arcToSegments(toX, toY, rx, ry, large, sweep, rotateX) { - var PI = Math.PI, th = rotateX * PI / 180, - sinTh = fabric.util.sin(th), - cosTh = fabric.util.cos(th), - fromX = 0, fromY = 0; - - rx = Math.abs(rx); - ry = Math.abs(ry); - - var px = -cosTh * toX * 0.5 - sinTh * toY * 0.5, - py = -cosTh * toY * 0.5 + sinTh * toX * 0.5, - rx2 = rx * rx, ry2 = ry * ry, py2 = py * py, px2 = px * px, - pl = rx2 * ry2 - rx2 * py2 - ry2 * px2, - root = 0; - - if (pl < 0) { - var s = Math.sqrt(1 - pl / (rx2 * ry2)); - rx *= s; - ry *= s; - } - else { - root = (large === sweep ? -1.0 : 1.0) * - Math.sqrt(pl / (rx2 * py2 + ry2 * px2)); - } - - var cx = root * rx * py / ry, - cy = -root * ry * px / rx, - cx1 = cosTh * cx - sinTh * cy + toX * 0.5, - cy1 = sinTh * cx + cosTh * cy + toY * 0.5, - mTheta = calcVectorAngle(1, 0, (px - cx) / rx, (py - cy) / ry), - dtheta = calcVectorAngle((px - cx) / rx, (py - cy) / ry, (-px - cx) / rx, (-py - cy) / ry); - - if (sweep === 0 && dtheta > 0) { - dtheta -= 2 * PI; - } - else if (sweep === 1 && dtheta < 0) { - dtheta += 2 * PI; - } - - // Convert into cubic bezier segments <= 90deg - var segments = Math.ceil(Math.abs(dtheta / PI * 2)), - result = [], mDelta = dtheta / segments, - mT = 8 / 3 * Math.sin(mDelta / 4) * Math.sin(mDelta / 4) / Math.sin(mDelta / 2), - th3 = mTheta + mDelta; - - for (var i = 0; i < segments; i++) { - result[i] = segmentToBezier(mTheta, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY); - fromX = result[i][5]; - fromY = result[i][6]; - mTheta = th3; - th3 += mDelta; - } - return result; - } - - /* - * Private - */ - function calcVectorAngle(ux, uy, vx, vy) { - var ta = Math.atan2(uy, ux), - tb = Math.atan2(vy, vx); - if (tb >= ta) { - return tb - ta; - } - else { - return 2 * Math.PI - (ta - tb); - } - } - - /** - * Calculate bounding box of a beziercurve - * @param {Number} x0 starting point - * @param {Number} y0 - * @param {Number} x1 first control point - * @param {Number} y1 - * @param {Number} x2 secondo control point - * @param {Number} y2 - * @param {Number} x3 end of bezier - * @param {Number} y3 - */ - // taken from http://jsbin.com/ivomiq/56/edit no credits available for that. - // TODO: can we normalize this with the starting points set at 0 and then translated the bbox? - function getBoundsOfCurve(x0, y0, x1, y1, x2, y2, x3, y3) { - var argsString; - if (fabric.cachesBoundsOfCurve) { - argsString = _join.call(arguments); - if (fabric.boundsOfCurveCache[argsString]) { - return fabric.boundsOfCurveCache[argsString]; - } - } - - var sqrt = Math.sqrt, - min = Math.min, max = Math.max, - abs = Math.abs, tvalues = [], - bounds = [[], []], - a, b, c, t, t1, t2, b2ac, sqrtb2ac; - - b = 6 * x0 - 12 * x1 + 6 * x2; - a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; - c = 3 * x1 - 3 * x0; - - for (var i = 0; i < 2; ++i) { - if (i > 0) { - b = 6 * y0 - 12 * y1 + 6 * y2; - a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; - c = 3 * y1 - 3 * y0; - } - - if (abs(a) < 1e-12) { - if (abs(b) < 1e-12) { - continue; - } - t = -c / b; - if (0 < t && t < 1) { - tvalues.push(t); - } - continue; - } - b2ac = b * b - 4 * c * a; - if (b2ac < 0) { - continue; - } - sqrtb2ac = sqrt(b2ac); - t1 = (-b + sqrtb2ac) / (2 * a); - if (0 < t1 && t1 < 1) { - tvalues.push(t1); - } - t2 = (-b - sqrtb2ac) / (2 * a); - if (0 < t2 && t2 < 1) { - tvalues.push(t2); - } - } - - var x, y, j = tvalues.length, jlen = j, mt; - while (j--) { - t = tvalues[j]; - mt = 1 - t; - x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); - bounds[0][j] = x; - - y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); - bounds[1][j] = y; - } - - bounds[0][jlen] = x0; - bounds[1][jlen] = y0; - bounds[0][jlen + 1] = x3; - bounds[1][jlen + 1] = y3; - var result = [ - { - x: min.apply(null, bounds[0]), - y: min.apply(null, bounds[1]) - }, - { - x: max.apply(null, bounds[0]), - y: max.apply(null, bounds[1]) - } - ]; - if (fabric.cachesBoundsOfCurve) { - fabric.boundsOfCurveCache[argsString] = result; - } - return result; - } - - /** - * Converts arc to a bunch of bezier curves - * @param {Number} fx starting point x - * @param {Number} fy starting point y - * @param {Array} coords Arc command - */ - function fromArcToBeziers(fx, fy, coords) { - var rx = coords[1], - ry = coords[2], - rot = coords[3], - large = coords[4], - sweep = coords[5], - tx = coords[6], - ty = coords[7], - segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); - - for (var i = 0, len = segsNorm.length; i < len; i++) { - segsNorm[i][1] += fx; - segsNorm[i][2] += fy; - segsNorm[i][3] += fx; - segsNorm[i][4] += fy; - segsNorm[i][5] += fx; - segsNorm[i][6] += fy; - } - return segsNorm; - }; - - /** - * This function take a parsed SVG path and make it simpler for fabricJS logic. - * simplification consist of: only UPPERCASE absolute commands ( relative converted to absolute ) - * S converted in C, T converted in Q, A converted in C. - * @param {Array} path the array of commands of a parsed svg path for fabric.Path - * @return {Array} the simplified array of commands of a parsed svg path for fabric.Path - */ - function makePathSimpler(path) { - // x and y represent the last point of the path. the previous command point. - // we add them to each relative command to make it an absolute comment. - // we also swap the v V h H with L, because are easier to transform. - var x = 0, y = 0, len = path.length, - // x1 and y1 represent the last point of the subpath. the subpath is started with - // m or M command. When a z or Z command is drawn, x and y need to be resetted to - // the last x1 and y1. - x1 = 0, y1 = 0, current, i, converted, - // previous will host the letter of the previous command, to handle S and T. - // controlX and controlY will host the previous reflected control point - destinationPath = [], previous, controlX, controlY; - for (i = 0; i < len; ++i) { - converted = false; - current = path[i].slice(0); - switch (current[0]) { // first letter - case 'l': // lineto, relative - current[0] = 'L'; - current[1] += x; - current[2] += y; - // falls through - case 'L': - x = current[1]; - y = current[2]; - break; - case 'h': // horizontal lineto, relative - current[1] += x; - // falls through - case 'H': - current[0] = 'L'; - current[2] = y; - x = current[1]; - break; - case 'v': // vertical lineto, relative - current[1] += y; - // falls through - case 'V': - current[0] = 'L'; - y = current[1]; - current[1] = x; - current[2] = y; - break; - case 'm': // moveTo, relative - current[0] = 'M'; - current[1] += x; - current[2] += y; - // falls through - case 'M': - x = current[1]; - y = current[2]; - x1 = current[1]; - y1 = current[2]; - break; - case 'c': // bezierCurveTo, relative - current[0] = 'C'; - current[1] += x; - current[2] += y; - current[3] += x; - current[4] += y; - current[5] += x; - current[6] += y; - // falls through - case 'C': - controlX = current[3]; - controlY = current[4]; - x = current[5]; - y = current[6]; - break; - case 's': // shorthand cubic bezierCurveTo, relative - current[0] = 'S'; - current[1] += x; - current[2] += y; - current[3] += x; - current[4] += y; - // falls through - case 'S': - // would be sScC but since we are swapping sSc for C, we check just that. - if (previous === 'C') { - // calculate reflection of previous control points - controlX = 2 * x - controlX; - controlY = 2 * y - controlY; - } - else { - // If there is no previous command or if the previous command was not a C, c, S, or s, - // the control point is coincident with the current point - controlX = x; - controlY = y; - } - x = current[3]; - y = current[4]; - current[0] = 'C'; - current[5] = current[3]; - current[6] = current[4]; - current[3] = current[1]; - current[4] = current[2]; - current[1] = controlX; - current[2] = controlY; - // current[3] and current[4] are NOW the second control point. - // we keep it for the next reflection. - controlX = current[3]; - controlY = current[4]; - break; - case 'q': // quadraticCurveTo, relative - current[0] = 'Q'; - current[1] += x; - current[2] += y; - current[3] += x; - current[4] += y; - // falls through - case 'Q': - controlX = current[1]; - controlY = current[2]; - x = current[3]; - y = current[4]; - break; - case 't': // shorthand quadraticCurveTo, relative - current[0] = 'T'; - current[1] += x; - current[2] += y; - // falls through - case 'T': - if (previous === 'Q') { - // calculate reflection of previous control point - controlX = 2 * x - controlX; - controlY = 2 * y - controlY; - } - else { - // If there is no previous command or if the previous command was not a Q, q, T or t, - // assume the control point is coincident with the current point - controlX = x; - controlY = y; - } - current[0] = 'Q'; - x = current[1]; - y = current[2]; - current[1] = controlX; - current[2] = controlY; - current[3] = x; - current[4] = y; - break; - case 'a': - current[0] = 'A'; - current[6] += x; - current[7] += y; - // falls through - case 'A': - converted = true; - destinationPath = destinationPath.concat(fromArcToBeziers(x, y, current)); - x = current[6]; - y = current[7]; - break; - case 'z': - case 'Z': - x = x1; - y = y1; - break; - default: - } - if (!converted) { - destinationPath.push(current); - } - previous = current[0]; - } - return destinationPath; - }; - - /** - * Calc length from point x1,y1 to x2,y2 - * @param {Number} x1 starting point x - * @param {Number} y1 starting point y - * @param {Number} x2 starting point x - * @param {Number} y2 starting point y - * @return {Number} length of segment - */ - function calcLineLength(x1, y1, x2, y2) { - return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); - } - - // functions for the Cubic beizer - // taken from: https://github.com/konvajs/konva/blob/7.0.5/src/shapes/Path.ts#L350 - function CB1(t) { - return t * t * t; - } - function CB2(t) { - return 3 * t * t * (1 - t); - } - function CB3(t) { - return 3 * t * (1 - t) * (1 - t); - } - function CB4(t) { - return (1 - t) * (1 - t) * (1 - t); - } - - function getPointOnCubicBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { - return function (pct) { - var c1 = CB1(pct), c2 = CB2(pct), c3 = CB3(pct), c4 = CB4(pct); - return { - x: p4x * c1 + p3x * c2 + p2x * c3 + p1x * c4, - y: p4y * c1 + p3y * c2 + p2y * c3 + p1y * c4 - }; - }; - } - - function getTangentCubicIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { - return function (pct) { - var invT = 1 - pct, - tangentX = (3 * invT * invT * (p2x - p1x)) + (6 * invT * pct * (p3x - p2x)) + - (3 * pct * pct * (p4x - p3x)), - tangentY = (3 * invT * invT * (p2y - p1y)) + (6 * invT * pct * (p3y - p2y)) + - (3 * pct * pct * (p4y - p3y)); - return Math.atan2(tangentY, tangentX); - }; - } - - function QB1(t) { - return t * t; - } - - function QB2(t) { - return 2 * t * (1 - t); - } - - function QB3(t) { - return (1 - t) * (1 - t); - } - - function getPointOnQuadraticBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y) { - return function (pct) { - var c1 = QB1(pct), c2 = QB2(pct), c3 = QB3(pct); - return { - x: p3x * c1 + p2x * c2 + p1x * c3, - y: p3y * c1 + p2y * c2 + p1y * c3 - }; - }; - } - - function getTangentQuadraticIterator(p1x, p1y, p2x, p2y, p3x, p3y) { - return function (pct) { - var invT = 1 - pct, - tangentX = (2 * invT * (p2x - p1x)) + (2 * pct * (p3x - p2x)), - tangentY = (2 * invT * (p2y - p1y)) + (2 * pct * (p3y - p2y)); - return Math.atan2(tangentY, tangentX); - }; - } - - - // this will run over a path segment ( a cubic or quadratic segment) and approximate it - // with 100 segemnts. This will good enough to calculate the length of the curve - function pathIterator(iterator, x1, y1) { - var tempP = { x: x1, y: y1 }, p, tmpLen = 0, perc; - for (perc = 1; perc <= 100; perc += 1) { - p = iterator(perc / 100); - tmpLen += calcLineLength(tempP.x, tempP.y, p.x, p.y); - tempP = p; - } - return tmpLen; - } - - /** - * Given a pathInfo, and a distance in pixels, find the percentage from 0 to 1 - * that correspond to that pixels run over the path. - * The percentage will be then used to find the correct point on the canvas for the path. - * @param {Array} segInfo fabricJS collection of information on a parsed path - * @param {Number} distance from starting point, in pixels. - * @return {Object} info object with x and y ( the point on canvas ) and angle, the tangent on that point; - */ - function findPercentageForDistance(segInfo, distance) { - var perc = 0, tmpLen = 0, iterator = segInfo.iterator, tempP = { x: segInfo.x, y: segInfo.y }, - p, nextLen, nextStep = 0.01, angleFinder = segInfo.angleFinder, lastPerc; - // nextStep > 0.0001 covers 0.00015625 that 1/64th of 1/100 - // the path - while (tmpLen < distance && nextStep > 0.0001) { - p = iterator(perc); - lastPerc = perc; - nextLen = calcLineLength(tempP.x, tempP.y, p.x, p.y); - // compare tmpLen each cycle with distance, decide next perc to test. - if ((nextLen + tmpLen) > distance) { - // we discard this step and we make smaller steps. - perc -= nextStep; - nextStep /= 2; - } - else { - tempP = p; - perc += nextStep; - tmpLen += nextLen; - } - } - p.angle = angleFinder(lastPerc); - return p; - } - - /** - * Run over a parsed and simplifed path and extrac some informations. - * informations are length of each command and starting point - * @param {Array} path fabricJS parsed path commands - * @return {Array} path commands informations - */ - function getPathSegmentsInfo(path) { - var totalLength = 0, len = path.length, current, - //x2 and y2 are the coords of segment start - //x1 and y1 are the coords of the current point - x1 = 0, y1 = 0, x2 = 0, y2 = 0, info = [], iterator, tempInfo, angleFinder; - for (var i = 0; i < len; i++) { - current = path[i]; - tempInfo = { - x: x1, - y: y1, - command: current[0], - }; - switch (current[0]) { //first letter - case 'M': - tempInfo.length = 0; - x2 = x1 = current[1]; - y2 = y1 = current[2]; - break; - case 'L': - tempInfo.length = calcLineLength(x1, y1, current[1], current[2]); - x1 = current[1]; - y1 = current[2]; - break; - case 'C': - iterator = getPointOnCubicBezierIterator( - x1, - y1, - current[1], - current[2], - current[3], - current[4], - current[5], - current[6] - ); - angleFinder = getTangentCubicIterator( - x1, - y1, - current[1], - current[2], - current[3], - current[4], - current[5], - current[6] - ); - tempInfo.iterator = iterator; - tempInfo.angleFinder = angleFinder; - tempInfo.length = pathIterator(iterator, x1, y1); - x1 = current[5]; - y1 = current[6]; - break; - case 'Q': - iterator = getPointOnQuadraticBezierIterator( - x1, - y1, - current[1], - current[2], - current[3], - current[4] - ); - angleFinder = getTangentQuadraticIterator( - x1, - y1, - current[1], - current[2], - current[3], - current[4] - ); - tempInfo.iterator = iterator; - tempInfo.angleFinder = angleFinder; - tempInfo.length = pathIterator(iterator, x1, y1); - x1 = current[3]; - y1 = current[4]; - break; - case 'Z': - case 'z': - // we add those in order to ease calculations later - tempInfo.destX = x2; - tempInfo.destY = y2; - tempInfo.length = calcLineLength(x1, y1, x2, y2); - x1 = x2; - y1 = y2; - break; - } - totalLength += tempInfo.length; - info.push(tempInfo); - } - info.push({ length: totalLength, x: x1, y: y1 }); - return info; - } - - function getPointOnPath(path, distance, infos) { - if (!infos) { - infos = getPathSegmentsInfo(path); - } - var i = 0; - while ((distance - infos[i].length > 0) && i < (infos.length - 2)) { - distance -= infos[i].length; - i++; - } - // var distance = infos[infos.length - 1] * perc; - var segInfo = infos[i], segPercent = distance / segInfo.length, - command = segInfo.command, segment = path[i], info; - - switch (command) { - case 'M': - return { x: segInfo.x, y: segInfo.y, angle: 0 }; - case 'Z': - case 'z': - info = new fabric.Point(segInfo.x, segInfo.y).lerp( - new fabric.Point(segInfo.destX, segInfo.destY), - segPercent - ); - info.angle = Math.atan2(segInfo.destY - segInfo.y, segInfo.destX - segInfo.x); - return info; - case 'L': - info = new fabric.Point(segInfo.x, segInfo.y).lerp( - new fabric.Point(segment[1], segment[2]), - segPercent - ); - info.angle = Math.atan2(segment[2] - segInfo.y, segment[1] - segInfo.x); - return info; - case 'C': - return findPercentageForDistance(segInfo, distance); - case 'Q': - return findPercentageForDistance(segInfo, distance); - } - } - - /** - * - * @param {string} pathString - * @return {(string|number)[][]} An array of SVG path commands - * @example Usage - * parsePath('M 3 4 Q 3 5 2 1 4 0 Q 9 12 2 1 4 0') === [ - * ['M', 3, 4], - * ['Q', 3, 5, 2, 1, 4, 0], - * ['Q', 9, 12, 2, 1, 4, 0], - * ]; - * - */ - function parsePath(pathString) { - var result = [], - coords = [], - currentPath, - parsed, - re = fabric.rePathCommand, - rNumber = '[-+]?(?:\\d*\\.\\d+|\\d+\\.?)(?:[eE][-+]?\\d+)?\\s*', - rNumberCommaWsp = '(' + rNumber + ')' + fabric.commaWsp, - rFlagCommaWsp = '([01])' + fabric.commaWsp + '?', - rArcSeq = rNumberCommaWsp + '?' + rNumberCommaWsp + '?' + rNumberCommaWsp + rFlagCommaWsp + rFlagCommaWsp + - rNumberCommaWsp + '?(' + rNumber + ')', - regArcArgumentSequence = new RegExp(rArcSeq, 'g'), - match, - coordsStr, - // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) - path; - if (!pathString || !pathString.match) { - return result; - } - path = pathString.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); - - for (var i = 0, coordsParsed, len = path.length; i < len; i++) { - currentPath = path[i]; - - coordsStr = currentPath.slice(1).trim(); - coords.length = 0; - - var command = currentPath.charAt(0); - coordsParsed = [command]; - - if (command.toLowerCase() === 'a') { - // arcs have special flags that apparently don't require spaces so handle special - for (var args; (args = regArcArgumentSequence.exec(coordsStr));) { - for (var j = 1; j < args.length; j++) { - coords.push(args[j]); - } - } - } - else { - while ((match = re.exec(coordsStr))) { - coords.push(match[0]); - } - } - - for (var j = 0, jlen = coords.length; j < jlen; j++) { - parsed = parseFloat(coords[j]); - if (!isNaN(parsed)) { - coordsParsed.push(parsed); - } - } - - var commandLength = commandLengths[command.toLowerCase()], - repeatedCommand = repeatedCommands[command] || command; - - if (coordsParsed.length - 1 > commandLength) { - for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) { - result.push([command].concat(coordsParsed.slice(k, k + commandLength))); - command = repeatedCommand; - } - } - else { - result.push(coordsParsed); - } - } - - return result; - }; - - /** - * - * Converts points to a smooth SVG path - * @param {{ x: number,y: number }[]} points Array of points - * @param {number} [correction] Apply a correction to the path (usually we use `width / 1000`). If value is undefined 0 is used as the correction value. - * @return {(string|number)[][]} An array of SVG path commands - */ - function getSmoothPathFromPoints(points, correction) { - var path = [], i, - p1 = new fabric.Point(points[0].x, points[0].y), - p2 = new fabric.Point(points[1].x, points[1].y), - len = points.length, multSignX = 1, multSignY = 0, manyPoints = len > 2; - correction = correction || 0; - - if (manyPoints) { - multSignX = points[2].x < p2.x ? -1 : points[2].x === p2.x ? 0 : 1; - multSignY = points[2].y < p2.y ? -1 : points[2].y === p2.y ? 0 : 1; - } - path.push(['M', p1.x - multSignX * correction, p1.y - multSignY * correction]); - for (i = 1; i < len; i++) { - if (!p1.eq(p2)) { - var midPoint = p1.midPointFrom(p2); - // p1 is our bezier control point - // midpoint is our endpoint - // start point is p(i-1) value. - path.push(['Q', p1.x, p1.y, midPoint.x, midPoint.y]); - } - p1 = points[i]; - if ((i + 1) < points.length) { - p2 = points[i + 1]; - } - } - if (manyPoints) { - multSignX = p1.x > points[i - 2].x ? 1 : p1.x === points[i - 2].x ? 0 : -1; - multSignY = p1.y > points[i - 2].y ? 1 : p1.y === points[i - 2].y ? 0 : -1; - } - path.push(['L', p1.x + multSignX * correction, p1.y + multSignY * correction]); - return path; - } - /** - * Transform a path by transforming each segment. - * it has to be a simplified path or it won't work. - * WARNING: this depends from pathOffset for correct operation - * @param {Array} path fabricJS parsed and simplified path commands - * @param {Array} transform matrix that represent the transformation - * @param {Object} [pathOffset] the fabric.Path pathOffset - * @param {Number} pathOffset.x - * @param {Number} pathOffset.y - * @returns {Array} the transformed path - */ - function transformPath(path, transform, pathOffset) { - if (pathOffset) { - transform = fabric.util.multiplyTransformMatrices( - transform, - [1, 0, 0, 1, -pathOffset.x, -pathOffset.y] - ); - } - return path.map(function (pathSegment) { - var newSegment = pathSegment.slice(0), point = {}; - for (var i = 1; i < pathSegment.length - 1; i += 2) { - point.x = pathSegment[i]; - point.y = pathSegment[i + 1]; - point = fabric.util.transformPoint(point, transform); - newSegment[i] = point.x; - newSegment[i + 1] = point.y; - } - return newSegment; - }); - } - - /** - * Join path commands to go back to svg format - * @param {Array} pathData fabricJS parsed path commands - * @return {String} joined path 'M 0 0 L 20 30' - */ - fabric.util.joinPath = function (pathData) { - return pathData.map(function (segment) { return segment.join(' '); }).join(' '); - }; - fabric.util.parsePath = parsePath; - fabric.util.makePathSimpler = makePathSimpler; - fabric.util.getSmoothPathFromPoints = getSmoothPathFromPoints; - fabric.util.getPathSegmentsInfo = getPathSegmentsInfo; - fabric.util.getBoundsOfCurve = getBoundsOfCurve; - fabric.util.getPointOnPath = getPointOnPath; - fabric.util.transformPath = transformPath; -})(); - - -(function () { - - var slice = Array.prototype.slice; - - /** - * Invokes method on all items in a given array - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} method Name of a method to invoke - * @return {Array} - */ - function invoke(array, method) { - var args = slice.call(arguments, 2), result = []; - for (var i = 0, len = array.length; i < len; i++) { - result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]); - } - return result; - } - - /** - * Finds maximum value in array (not necessarily "first" one) - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} byProperty - * @return {*} - */ - function max(array, byProperty) { - return find(array, byProperty, function (value1, value2) { - return value1 >= value2; - }); - } - - /** - * Finds minimum value in array (not necessarily "first" one) - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} byProperty - * @return {*} - */ - function min(array, byProperty) { - return find(array, byProperty, function (value1, value2) { - return value1 < value2; - }); - } - - /** - * @private - */ - function fill(array, value) { - var k = array.length; - while (k--) { - array[k] = value; - } - return array; - } - - /** - * @private - */ - function find(array, byProperty, condition) { - if (!array || array.length === 0) { - return; - } - - var i = array.length - 1, - result = byProperty ? array[i][byProperty] : array[i]; - if (byProperty) { - while (i--) { - if (condition(array[i][byProperty], result)) { - result = array[i][byProperty]; - } - } - } - else { - while (i--) { - if (condition(array[i], result)) { - result = array[i]; - } - } - } - return result; - } - - /** - * @namespace fabric.util.array - */ - fabric.util.array = { - fill: fill, - invoke: invoke, - min: min, - max: max - }; - -})(); - - -(function () { - /** - * Copies all enumerable properties of one js object to another - * this does not and cannot compete with generic utils. - * Does not clone or extend fabric.Object subclasses. - * This is mostly for internal use and has extra handling for fabricJS objects - * it skips the canvas and group properties in deep cloning. - * @memberOf fabric.util.object - * @param {Object} destination Where to copy to - * @param {Object} source Where to copy from - * @param {Boolean} [deep] Whether to extend nested objects - * @return {Object} - */ - - function extend(destination, source, deep) { - // JScript DontEnum bug is not taken care of - // the deep clone is for internal use, is not meant to avoid - // javascript traps or cloning html element or self referenced objects. - if (deep) { - if (!fabric.isLikelyNode && source instanceof Element) { - // avoid cloning deep images, canvases, - destination = source; - } - else if (source instanceof Array) { - destination = []; - for (var i = 0, len = source.length; i < len; i++) { - destination[i] = extend({}, source[i], deep); - } - } - else if (source && typeof source === 'object') { - for (var property in source) { - if (property === 'canvas' || property === 'group') { - // we do not want to clone this props at all. - // we want to keep the keys in the copy - destination[property] = null; - } - else if (source.hasOwnProperty(property)) { - destination[property] = extend({}, source[property], deep); - } - } - } - else { - // this sounds odd for an extend but is ok for recursive use - destination = source; - } - } - else { - for (var property in source) { - destination[property] = source[property]; - } - } - return destination; - } - - /** - * Creates an empty object and copies all enumerable properties of another object to it - * This method is mostly for internal use, and not intended for duplicating shapes in canvas. - * @memberOf fabric.util.object - * @param {Object} object Object to clone - * @param {Boolean} [deep] Whether to clone nested objects - * @return {Object} - */ - - //TODO: this function return an empty object if you try to clone null - function clone(object, deep) { - return extend({}, object, deep); - } - - /** @namespace fabric.util.object */ - fabric.util.object = { - extend: extend, - clone: clone - }; - fabric.util.object.extend(fabric.util, fabric.Observable); -})(); - - -(function () { - - /** - * Camelizes a string - * @memberOf fabric.util.string - * @param {String} string String to camelize - * @return {String} Camelized version of a string - */ - function camelize(string) { - return string.replace(/-+(.)?/g, function (match, character) { - return character ? character.toUpperCase() : ''; - }); - } - - /** - * Capitalizes a string - * @memberOf fabric.util.string - * @param {String} string String to capitalize - * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized - * and other letters stay untouched, if false first letter is capitalized - * and other letters are converted to lowercase. - * @return {String} Capitalized version of a string - */ - function capitalize(string, firstLetterOnly) { - return string.charAt(0).toUpperCase() + - (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); - } - - /** - * Escapes XML in a string - * @memberOf fabric.util.string - * @param {String} string String to escape - * @return {String} Escaped version of a string - */ - function escapeXml(string) { - return string.replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>'); - } - - /** - * Divide a string in the user perceived single units - * @memberOf fabric.util.string - * @param {String} textstring String to escape - * @return {Array} array containing the graphemes - */ - function graphemeSplit(textstring) { - var i = 0, chr, graphemes = []; - for (i = 0, chr; i < textstring.length; i++) { - if ((chr = getWholeChar(textstring, i)) === false) { - continue; - } - graphemes.push(chr); - } - return graphemes; - } - - // taken from mdn in the charAt doc page. - function getWholeChar(str, i) { - var code = str.charCodeAt(i); - - if (isNaN(code)) { - return ''; // Position not found - } - if (code < 0xD800 || code > 0xDFFF) { - return str.charAt(i); - } - - // High surrogate (could change last hex to 0xDB7F to treat high private - // surrogates as single characters) - if (0xD800 <= code && code <= 0xDBFF) { - if (str.length <= (i + 1)) { - throw 'High surrogate without following low surrogate'; - } - var next = str.charCodeAt(i + 1); - if (0xDC00 > next || next > 0xDFFF) { - throw 'High surrogate without following low surrogate'; - } - return str.charAt(i) + str.charAt(i + 1); - } - // Low surrogate (0xDC00 <= code && code <= 0xDFFF) - if (i === 0) { - throw 'Low surrogate without preceding high surrogate'; - } - var prev = str.charCodeAt(i - 1); - - // (could change last hex to 0xDB7F to treat high private - // surrogates as single characters) - if (0xD800 > prev || prev > 0xDBFF) { - throw 'Low surrogate without preceding high surrogate'; - } - // We can pass over low surrogates now as the second component - // in a pair which we have already processed - return false; - } - - - /** - * String utilities - * @namespace fabric.util.string - */ - fabric.util.string = { - camelize: camelize, - capitalize: capitalize, - escapeXml: escapeXml, - graphemeSplit: graphemeSplit - }; -})(); - - -(function () { - - var slice = Array.prototype.slice, emptyFunction = function () { }, - - IS_DONTENUM_BUGGY = (function () { - for (var p in { toString: 1 }) { - if (p === 'toString') { - return false; - } - } - return true; - })(), - - /** @ignore */ - addMethods = function (klass, source, parent) { - for (var property in source) { - - if (property in klass.prototype && - typeof klass.prototype[property] === 'function' && - (source[property] + '').indexOf('callSuper') > -1) { - - klass.prototype[property] = (function (property) { - return function () { - - var superclass = this.constructor.superclass; - this.constructor.superclass = parent; - var returnValue = source[property].apply(this, arguments); - this.constructor.superclass = superclass; - - if (property !== 'initialize') { - return returnValue; - } - }; - })(property); - } - else { - klass.prototype[property] = source[property]; - } - - if (IS_DONTENUM_BUGGY) { - if (source.toString !== Object.prototype.toString) { - klass.prototype.toString = source.toString; - } - if (source.valueOf !== Object.prototype.valueOf) { - klass.prototype.valueOf = source.valueOf; - } - } - } - }; - - function Subclass() { } - - function callSuper(methodName) { - var parentMethod = null, - _this = this; - - // climb prototype chain to find method not equal to callee's method - while (_this.constructor.superclass) { - var superClassMethod = _this.constructor.superclass.prototype[methodName]; - if (_this[methodName] !== superClassMethod) { - parentMethod = superClassMethod; - break; - } - // eslint-disable-next-line - _this = _this.constructor.superclass.prototype; - } - - if (!parentMethod) { - return console.log('tried to callSuper ' + methodName + ', method not found in prototype chain', this); - } - - return (arguments.length > 1) - ? parentMethod.apply(this, slice.call(arguments, 1)) - : parentMethod.call(this); - } - - /** - * Helper for creation of "classes". - * @memberOf fabric.util - * @param {Function} [parent] optional "Class" to inherit from - * @param {Object} [properties] Properties shared by all instances of this class - * (be careful modifying objects defined here as this would affect all instances) - */ - function createClass() { - var parent = null, - properties = slice.call(arguments, 0); - - if (typeof properties[0] === 'function') { - parent = properties.shift(); - } - function klass() { - this.initialize.apply(this, arguments); - } - - klass.superclass = parent; - klass.subclasses = []; - - if (parent) { - Subclass.prototype = parent.prototype; - klass.prototype = new Subclass(); - parent.subclasses.push(klass); - } - for (var i = 0, length = properties.length; i < length; i++) { - addMethods(klass, properties[i], parent); - } - if (!klass.prototype.initialize) { - klass.prototype.initialize = emptyFunction; - } - klass.prototype.constructor = klass; - klass.prototype.callSuper = callSuper; - return klass; - } - - fabric.util.createClass = createClass; -})(); - - -(function () { - // since ie11 can use addEventListener but they do not support options, i need to check - var couldUseAttachEvent = !!fabric.document.createElement('div').attachEvent, - touchEvents = ['touchstart', 'touchmove', 'touchend']; - /** - * Adds an event listener to an element - * @function - * @memberOf fabric.util - * @param {HTMLElement} element - * @param {String} eventName - * @param {Function} handler - */ - fabric.util.addListener = function (element, eventName, handler, options) { - element && element.addEventListener(eventName, handler, couldUseAttachEvent ? false : options); - }; - - /** - * Removes an event listener from an element - * @function - * @memberOf fabric.util - * @param {HTMLElement} element - * @param {String} eventName - * @param {Function} handler - */ - fabric.util.removeListener = function (element, eventName, handler, options) { - element && element.removeEventListener(eventName, handler, couldUseAttachEvent ? false : options); - }; - - function getTouchInfo(event) { - var touchProp = event.changedTouches; - if (touchProp && touchProp[0]) { - return touchProp[0]; - } - return event; - } - - fabric.util.getPointer = function (event) { - var element = event.target, - scroll = fabric.util.getScrollLeftTop(element), - _evt = getTouchInfo(event); - return { - x: _evt.clientX + scroll.left, - y: _evt.clientY + scroll.top - }; - }; - - fabric.util.isTouchEvent = function (event) { - return touchEvents.indexOf(event.type) > -1 || event.pointerType === 'touch'; - }; -})(); - - -(function () { - - /** - * Cross-browser wrapper for setting element's style - * @memberOf fabric.util - * @param {HTMLElement} element - * @param {Object} styles - * @return {HTMLElement} Element that was passed as a first argument - */ - function setStyle(element, styles) { - var elementStyle = element.style; - if (!elementStyle) { - return element; - } - if (typeof styles === 'string') { - element.style.cssText += ';' + styles; - return styles.indexOf('opacity') > -1 - ? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) - : element; - } - for (var property in styles) { - if (property === 'opacity') { - setOpacity(element, styles[property]); - } - else { - var normalizedProperty = (property === 'float' || property === 'cssFloat') - ? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat') - : property; - elementStyle.setProperty(normalizedProperty, styles[property]); - } - } - return element; - } - - var parseEl = fabric.document.createElement('div'), - supportsOpacity = typeof parseEl.style.opacity === 'string', - supportsFilters = typeof parseEl.style.filter === 'string', - reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, - - /** @ignore */ - setOpacity = function (element) { return element; }; - - if (supportsOpacity) { - /** @ignore */ - setOpacity = function (element, value) { - element.style.opacity = value; - return element; - }; - } - else if (supportsFilters) { - /** @ignore */ - setOpacity = function (element, value) { - var es = element.style; - if (element.currentStyle && !element.currentStyle.hasLayout) { - es.zoom = 1; - } - if (reOpacity.test(es.filter)) { - value = value >= 0.9999 ? '' : ('alpha(opacity=' + (value * 100) + ')'); - es.filter = es.filter.replace(reOpacity, value); - } - else { - es.filter += ' alpha(opacity=' + (value * 100) + ')'; - } - return element; - }; - } - - fabric.util.setStyle = setStyle; - -})(); - - -(function () { - - var _slice = Array.prototype.slice; - - /** - * Takes id and returns an element with that id (if one exists in a document) - * @memberOf fabric.util - * @param {String|HTMLElement} id - * @return {HTMLElement|null} - */ - function getById(id) { - return typeof id === 'string' ? fabric.document.getElementById(id) : id; - } - - var sliceCanConvertNodelists, - /** - * Converts an array-like object (e.g. arguments or NodeList) to an array - * @memberOf fabric.util - * @param {Object} arrayLike - * @return {Array} - */ - toArray = function (arrayLike) { - return _slice.call(arrayLike, 0); - }; - - try { - sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; - } - catch (err) { } - - if (!sliceCanConvertNodelists) { - toArray = function (arrayLike) { - var arr = new Array(arrayLike.length), i = arrayLike.length; - while (i--) { - arr[i] = arrayLike[i]; - } - return arr; - }; - } - - /** - * Creates specified element with specified attributes - * @memberOf fabric.util - * @param {String} tagName Type of an element to create - * @param {Object} [attributes] Attributes to set on an element - * @return {HTMLElement} Newly created element - */ - function makeElement(tagName, attributes) { - var el = fabric.document.createElement(tagName); - for (var prop in attributes) { - if (prop === 'class') { - el.className = attributes[prop]; - } - else if (prop === 'for') { - el.htmlFor = attributes[prop]; - } - else { - el.setAttribute(prop, attributes[prop]); - } - } - return el; - } - - /** - * Adds class to an element - * @memberOf fabric.util - * @param {HTMLElement} element Element to add class to - * @param {String} className Class to add to an element - */ - function addClass(element, className) { - if (element && (' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { - element.className += (element.className ? ' ' : '') + className; - } - } - - /** - * Wraps element with another element - * @memberOf fabric.util - * @param {HTMLElement} element Element to wrap - * @param {HTMLElement|String} wrapper Element to wrap with - * @param {Object} [attributes] Attributes to set on a wrapper - * @return {HTMLElement} wrapper - */ - function wrapElement(element, wrapper, attributes) { - if (typeof wrapper === 'string') { - wrapper = makeElement(wrapper, attributes); - } - if (element.parentNode) { - element.parentNode.replaceChild(wrapper, element); - } - wrapper.appendChild(element); - return wrapper; - } - - /** - * Returns element scroll offsets - * @memberOf fabric.util - * @param {HTMLElement} element Element to operate on - * @return {Object} Object with left/top values - */ - function getScrollLeftTop(element) { - - var left = 0, - top = 0, - docElement = fabric.document.documentElement, - body = fabric.document.body || { - scrollLeft: 0, scrollTop: 0 - }; - - // While loop checks (and then sets element to) .parentNode OR .host - // to account for ShadowDOM. We still want to traverse up out of ShadowDOM, - // but the .parentNode of a root ShadowDOM node will always be null, instead - // it should be accessed through .host. See http://stackoverflow.com/a/24765528/4383938 - while (element && (element.parentNode || element.host)) { - - // Set element to element parent, or 'host' in case of ShadowDOM - element = element.parentNode || element.host; - - if (element === fabric.document) { - left = body.scrollLeft || docElement.scrollLeft || 0; - top = body.scrollTop || docElement.scrollTop || 0; - } - else { - left += element.scrollLeft || 0; - top += element.scrollTop || 0; - } - - if (element.nodeType === 1 && element.style.position === 'fixed') { - break; - } - } - - return { left: left, top: top }; - } - - /** - * Returns offset for a given element - * @function - * @memberOf fabric.util - * @param {HTMLElement} element Element to get offset for - * @return {Object} Object with "left" and "top" properties - */ - function getElementOffset(element) { - var docElem, - doc = element && element.ownerDocument, - box = { left: 0, top: 0 }, - offset = { left: 0, top: 0 }, - scrollLeftTop, - offsetAttributes = { - borderLeftWidth: 'left', - borderTopWidth: 'top', - paddingLeft: 'left', - paddingTop: 'top' - }; - - if (!doc) { - return offset; - } - - for (var attr in offsetAttributes) { - offset[offsetAttributes[attr]] += parseInt(getElementStyle(element, attr), 10) || 0; - } - - docElem = doc.documentElement; - if (typeof element.getBoundingClientRect !== 'undefined') { - box = element.getBoundingClientRect(); - } - - scrollLeftTop = getScrollLeftTop(element); - - return { - left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, - top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top - }; - } - - /** - * Returns style attribute value of a given element - * @memberOf fabric.util - * @param {HTMLElement} element Element to get style attribute for - * @param {String} attr Style attribute to get for element - * @return {String} Style attribute value of the given element. - */ - var getElementStyle; - if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) { - getElementStyle = function (element, attr) { - var style = fabric.document.defaultView.getComputedStyle(element, null); - return style ? style[attr] : undefined; - }; - } - else { - getElementStyle = function (element, attr) { - var value = element.style[attr]; - if (!value && element.currentStyle) { - value = element.currentStyle[attr]; - } - return value; - }; - } - - (function () { - var style = fabric.document.documentElement.style, - selectProp = 'userSelect' in style - ? 'userSelect' - : 'MozUserSelect' in style - ? 'MozUserSelect' - : 'WebkitUserSelect' in style - ? 'WebkitUserSelect' - : 'KhtmlUserSelect' in style - ? 'KhtmlUserSelect' - : ''; - - /** - * Makes element unselectable - * @memberOf fabric.util - * @param {HTMLElement} element Element to make unselectable - * @return {HTMLElement} Element that was passed in - */ - function makeElementUnselectable(element) { - if (typeof element.onselectstart !== 'undefined') { - element.onselectstart = fabric.util.falseFunction; - } - if (selectProp) { - element.style[selectProp] = 'none'; - } - else if (typeof element.unselectable === 'string') { - element.unselectable = 'on'; - } - return element; - } - - /** - * Makes element selectable - * @memberOf fabric.util - * @param {HTMLElement} element Element to make selectable - * @return {HTMLElement} Element that was passed in - */ - function makeElementSelectable(element) { - if (typeof element.onselectstart !== 'undefined') { - element.onselectstart = null; - } - if (selectProp) { - element.style[selectProp] = ''; - } - else if (typeof element.unselectable === 'string') { - element.unselectable = ''; - } - return element; - } - - fabric.util.makeElementUnselectable = makeElementUnselectable; - fabric.util.makeElementSelectable = makeElementSelectable; - })(); - - function getNodeCanvas(element) { - var impl = fabric.jsdomImplForWrapper(element); - return impl._canvas || impl._image; - }; - - function cleanUpJsdomNode(element) { - if (!fabric.isLikelyNode) { - return; - } - var impl = fabric.jsdomImplForWrapper(element); - if (impl) { - impl._image = null; - impl._canvas = null; - // unsure if necessary - impl._currentSrc = null; - impl._attributes = null; - impl._classList = null; - } - } - - function setImageSmoothing(ctx, value) { - ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled - || ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled; - ctx.imageSmoothingEnabled = value; - } - - /** - * setImageSmoothing sets the context imageSmoothingEnabled property. - * Used by canvas and by ImageObject. - * @memberOf fabric.util - * @since 4.0.0 - * @param {HTMLRenderingContext2D} ctx to set on - * @param {Boolean} value true or false - */ - fabric.util.setImageSmoothing = setImageSmoothing; - fabric.util.getById = getById; - fabric.util.toArray = toArray; - fabric.util.addClass = addClass; - fabric.util.makeElement = makeElement; - fabric.util.wrapElement = wrapElement; - fabric.util.getScrollLeftTop = getScrollLeftTop; - fabric.util.getElementOffset = getElementOffset; - fabric.util.getNodeCanvas = getNodeCanvas; - fabric.util.cleanUpJsdomNode = cleanUpJsdomNode; - -})(); - - -(function () { - - function addParamToUrl(url, param) { - return url + (/\?/.test(url) ? '&' : '?') + param; - } - - function emptyFn() { } - - /** - * Cross-browser abstraction for sending XMLHttpRequest - * @memberOf fabric.util - * @param {String} url URL to send XMLHttpRequest to - * @param {Object} [options] Options object - * @param {String} [options.method="GET"] - * @param {String} [options.parameters] parameters to append to url in GET or in body - * @param {String} [options.body] body to send with POST or PUT request - * @param {Function} options.onComplete Callback to invoke when request is completed - * @return {XMLHttpRequest} request - */ - function request(url, options) { - options || (options = {}); - - var method = options.method ? options.method.toUpperCase() : 'GET', - onComplete = options.onComplete || function () { }, - xhr = new fabric.window.XMLHttpRequest(), - body = options.body || options.parameters; - - /** @ignore */ - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - onComplete(xhr); - xhr.onreadystatechange = emptyFn; - } - }; - - if (method === 'GET') { - body = null; - if (typeof options.parameters === 'string') { - url = addParamToUrl(url, options.parameters); - } - } - - xhr.open(method, url, true); - - if (method === 'POST' || method === 'PUT') { - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - } - - xhr.send(body); - return xhr; - } - - fabric.util.request = request; -})(); - - -/** - * Wrapper around `console.log` (when available) - * @param {*} [values] Values to log - */ -fabric.log = console.log; - -/** - * Wrapper around `console.warn` (when available) - * @param {*} [values] Values to log as a warning - */ -fabric.warn = console.warn; - - -(function () { - - var extend = fabric.util.object.extend, - clone = fabric.util.object.clone; - - /** - * @typedef {Object} AnimationOptions - * Animation of a value or list of values. - * When using lists, think of something like this: - * fabric.util.animate({ - * startValue: [1, 2, 3], - * endValue: [2, 4, 6], - * onChange: function([a, b, c]) { - * canvas.zoomToPoint({x: b, y: c}, a) - * canvas.renderAll() - * } - * }); - * @example - * @property {Function} [onChange] Callback; invoked on every value change - * @property {Function} [onComplete] Callback; invoked when value change is completed - * @example - * // Note: startValue, endValue, and byValue must match the type - * var animationOptions = { startValue: 0, endValue: 1, byValue: 0.25 } - * var animationOptions = { startValue: [0, 1], endValue: [1, 2], byValue: [0.25, 0.25] } - * @property {number | number[]} [startValue=0] Starting value - * @property {number | number[]} [endValue=100] Ending value - * @property {number | number[]} [byValue=100] Value to modify the property by - * @property {Function} [easing] Easing function - * @property {Number} [duration=500] Duration of change (in ms) - * @property {Function} [abort] Additional function with logic. If returns true, animation aborts. - * - * @typedef {() => void} CancelFunction - * - * @typedef {Object} AnimationCurrentState - * @property {number | number[]} currentValue value in range [`startValue`, `endValue`] - * @property {number} completionRate value in range [0, 1] - * @property {number} durationRate value in range [0, 1] - * - * @typedef {(AnimationOptions & AnimationCurrentState & { cancel: CancelFunction }} AnimationContext - */ - - /** - * Array holding all running animations - * @memberof fabric - * @type {AnimationContext[]} - */ - var RUNNING_ANIMATIONS = []; - fabric.util.object.extend(RUNNING_ANIMATIONS, { - - /** - * cancel all running animations at the next requestAnimFrame - * @returns {AnimationContext[]} - */ - cancelAll: function () { - var animations = this.splice(0); - animations.forEach(function (animation) { - animation.cancel(); - }); - return animations; - }, - - /** - * cancel all running animations attached to canvas at the next requestAnimFrame - * @param {fabric.Canvas} canvas - * @returns {AnimationContext[]} - */ - cancelByCanvas: function (canvas) { - if (!canvas) { - return []; - } - var cancelled = this.filter(function (animation) { - return typeof animation.target === 'object' && animation.target.canvas === canvas; - }); - cancelled.forEach(function (animation) { - animation.cancel(); - }); - return cancelled; - }, - - /** - * cancel all running animations for target at the next requestAnimFrame - * @param {*} target - * @returns {AnimationContext[]} - */ - cancelByTarget: function (target) { - var cancelled = this.findAnimationsByTarget(target); - cancelled.forEach(function (animation) { - animation.cancel(); - }); - return cancelled; - }, - - /** - * - * @param {CancelFunction} cancelFunc the function returned by animate - * @returns {number} - */ - findAnimationIndex: function (cancelFunc) { - return this.indexOf(this.findAnimation(cancelFunc)); - }, - - /** - * - * @param {CancelFunction} cancelFunc the function returned by animate - * @returns {AnimationContext | undefined} animation's options object - */ - findAnimation: function (cancelFunc) { - return this.find(function (animation) { - return animation.cancel === cancelFunc; - }); - }, - - /** - * - * @param {*} target the object that is assigned to the target property of the animation context - * @returns {AnimationContext[]} array of animation options object associated with target - */ - findAnimationsByTarget: function (target) { - if (!target) { - return []; - } - return this.filter(function (animation) { - return animation.target === target; - }); - } - }); - - function noop() { - return false; - } - - function defaultEasing(t, b, c, d) { - return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; - } - - /** - * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. - * @memberOf fabric.util - * @param {AnimationOptions} [options] Animation options - * @example - * // Note: startValue, endValue, and byValue must match the type - * fabric.util.animate({ startValue: 0, endValue: 1, byValue: 0.25 }) - * fabric.util.animate({ startValue: [0, 1], endValue: [1, 2], byValue: [0.25, 0.25] }) - * @returns {CancelFunction} cancel function - */ - function animate(options) { - options || (options = {}); - var cancel = false, - context, - removeFromRegistry = function () { - var index = fabric.runningAnimations.indexOf(context); - return index > -1 && fabric.runningAnimations.splice(index, 1)[0]; - }; - - context = extend(clone(options), { - cancel: function () { - cancel = true; - return removeFromRegistry(); - }, - currentValue: 'startValue' in options ? options.startValue : 0, - completionRate: 0, - durationRate: 0 - }); - fabric.runningAnimations.push(context); - - requestAnimFrame(function (timestamp) { - var start = timestamp || +new Date(), - duration = options.duration || 500, - finish = start + duration, time, - onChange = options.onChange || noop, - abort = options.abort || noop, - onComplete = options.onComplete || noop, - easing = options.easing || defaultEasing, - isMany = 'startValue' in options ? options.startValue.length > 0 : false, - startValue = 'startValue' in options ? options.startValue : 0, - endValue = 'endValue' in options ? options.endValue : 100, - byValue = options.byValue || (isMany ? startValue.map(function (value, i) { - return endValue[i] - startValue[i]; - }) : endValue - startValue); - - options.onStart && options.onStart(); - - (function tick(ticktime) { - time = ticktime || +new Date(); - var currentTime = time > finish ? duration : (time - start), - timePerc = currentTime / duration, - current = isMany ? startValue.map(function (_value, i) { - return easing(currentTime, startValue[i], byValue[i], duration); - }) : easing(currentTime, startValue, byValue, duration), - valuePerc = isMany ? Math.abs((current[0] - startValue[0]) / byValue[0]) - : Math.abs((current - startValue) / byValue); - // update context - context.currentValue = isMany ? current.slice() : current; - context.completionRate = valuePerc; - context.durationRate = timePerc; - if (cancel) { - return; - } - if (abort(current, valuePerc, timePerc)) { - removeFromRegistry(); - return; - } - if (time > finish) { - // update context - context.currentValue = isMany ? endValue.slice() : endValue; - context.completionRate = 1; - context.durationRate = 1; - // execute callbacks - onChange(isMany ? endValue.slice() : endValue, 1, 1); - onComplete(endValue, 1, 1); - removeFromRegistry(); - return; - } - else { - onChange(current, valuePerc, timePerc); - requestAnimFrame(tick); - } - })(start); - }); - - return context.cancel; - } - - var _requestAnimFrame = fabric.window.requestAnimationFrame || - fabric.window.webkitRequestAnimationFrame || - fabric.window.mozRequestAnimationFrame || - fabric.window.oRequestAnimationFrame || - fabric.window.msRequestAnimationFrame || - function (callback) { - return fabric.window.setTimeout(callback, 1000 / 60); - }; - - var _cancelAnimFrame = fabric.window.cancelAnimationFrame || fabric.window.clearTimeout; - - /** - * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ - * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method - * @memberOf fabric.util - * @param {Function} callback Callback to invoke - * @param {DOMElement} element optional Element to associate with animation - */ - function requestAnimFrame() { - return _requestAnimFrame.apply(fabric.window, arguments); - } - - function cancelAnimFrame() { - return _cancelAnimFrame.apply(fabric.window, arguments); - } - - fabric.util.animate = animate; - fabric.util.requestAnimFrame = requestAnimFrame; - fabric.util.cancelAnimFrame = cancelAnimFrame; - fabric.runningAnimations = RUNNING_ANIMATIONS; -})(); - - -(function () { - // Calculate an in-between color. Returns a "rgba()" string. - // Credit: Edwin Martin - // http://www.bitstorm.org/jquery/color-animation/jquery.animate-colors.js - function calculateColor(begin, end, pos) { - var color = 'rgba(' - + parseInt((begin[0] + pos * (end[0] - begin[0])), 10) + ',' - + parseInt((begin[1] + pos * (end[1] - begin[1])), 10) + ',' - + parseInt((begin[2] + pos * (end[2] - begin[2])), 10); - - color += ',' + (begin && end ? parseFloat(begin[3] + pos * (end[3] - begin[3])) : 1); - color += ')'; - return color; - } - - /** - * Changes the color from one to another within certain period of time, invoking callbacks as value is being changed. - * @memberOf fabric.util - * @param {String} fromColor The starting color in hex or rgb(a) format. - * @param {String} toColor The starting color in hex or rgb(a) format. - * @param {Number} [duration] Duration of change (in ms). - * @param {Object} [options] Animation options - * @param {Function} [options.onChange] Callback; invoked on every value change - * @param {Function} [options.onComplete] Callback; invoked when value change is completed - * @param {Function} [options.colorEasing] Easing function. Note that this function only take two arguments (currentTime, duration). Thus the regular animation easing functions cannot be used. - * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. - * @returns {Function} abort function - */ - function animateColor(fromColor, toColor, duration, options) { - var startColor = new fabric.Color(fromColor).getSource(), - endColor = new fabric.Color(toColor).getSource(), - originalOnComplete = options.onComplete, - originalOnChange = options.onChange; - options = options || {}; - - return fabric.util.animate(fabric.util.object.extend(options, { - duration: duration || 500, - startValue: startColor, - endValue: endColor, - byValue: endColor, - easing: function (currentTime, startValue, byValue, duration) { - var posValue = options.colorEasing - ? options.colorEasing(currentTime, duration) - : 1 - Math.cos(currentTime / duration * (Math.PI / 2)); - return calculateColor(startValue, byValue, posValue); - }, - // has to take in account for color restoring; - onComplete: function (current, valuePerc, timePerc) { - if (originalOnComplete) { - return originalOnComplete( - calculateColor(endColor, endColor, 0), - valuePerc, - timePerc - ); - } - }, - onChange: function (current, valuePerc, timePerc) { - if (originalOnChange) { - if (Array.isArray(current)) { - return originalOnChange( - calculateColor(current, current, 0), - valuePerc, - timePerc - ); - } - originalOnChange(current, valuePerc, timePerc); - } - } - })); - } - - fabric.util.animateColor = animateColor; - -})(); - - -(function () { - - function normalize(a, c, p, s) { - if (a < Math.abs(c)) { - a = c; - s = p / 4; - } - else { - //handle the 0/0 case: - if (c === 0 && a === 0) { - s = p / (2 * Math.PI) * Math.asin(1); - } - else { - s = p / (2 * Math.PI) * Math.asin(c / a); - } - } - return { a: a, c: c, p: p, s: s }; - } - - function elastic(opts, t, d) { - return opts.a * - Math.pow(2, 10 * (t -= 1)) * - Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p); - } - - /** - * Cubic easing out - * @memberOf fabric.util.ease - */ - function easeOutCubic(t, b, c, d) { - return c * ((t = t / d - 1) * t * t + 1) + b; - } - - /** - * Cubic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutCubic(t, b, c, d) { - t /= d / 2; - if (t < 1) { - return c / 2 * t * t * t + b; - } - return c / 2 * ((t -= 2) * t * t + 2) + b; - } - - /** - * Quartic easing in - * @memberOf fabric.util.ease - */ - function easeInQuart(t, b, c, d) { - return c * (t /= d) * t * t * t + b; - } - - /** - * Quartic easing out - * @memberOf fabric.util.ease - */ - function easeOutQuart(t, b, c, d) { - return -c * ((t = t / d - 1) * t * t * t - 1) + b; - } - - /** - * Quartic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutQuart(t, b, c, d) { - t /= d / 2; - if (t < 1) { - return c / 2 * t * t * t * t + b; - } - return -c / 2 * ((t -= 2) * t * t * t - 2) + b; - } - - /** - * Quintic easing in - * @memberOf fabric.util.ease - */ - function easeInQuint(t, b, c, d) { - return c * (t /= d) * t * t * t * t + b; - } - - /** - * Quintic easing out - * @memberOf fabric.util.ease - */ - function easeOutQuint(t, b, c, d) { - return c * ((t = t / d - 1) * t * t * t * t + 1) + b; - } - - /** - * Quintic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutQuint(t, b, c, d) { - t /= d / 2; - if (t < 1) { - return c / 2 * t * t * t * t * t + b; - } - return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; - } - - /** - * Sinusoidal easing in - * @memberOf fabric.util.ease - */ - function easeInSine(t, b, c, d) { - return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; - } - - /** - * Sinusoidal easing out - * @memberOf fabric.util.ease - */ - function easeOutSine(t, b, c, d) { - return c * Math.sin(t / d * (Math.PI / 2)) + b; - } - - /** - * Sinusoidal easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutSine(t, b, c, d) { - return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; - } - - /** - * Exponential easing in - * @memberOf fabric.util.ease - */ - function easeInExpo(t, b, c, d) { - return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; - } - - /** - * Exponential easing out - * @memberOf fabric.util.ease - */ - function easeOutExpo(t, b, c, d) { - return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; - } - - /** - * Exponential easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutExpo(t, b, c, d) { - if (t === 0) { - return b; - } - if (t === d) { - return b + c; - } - t /= d / 2; - if (t < 1) { - return c / 2 * Math.pow(2, 10 * (t - 1)) + b; - } - return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; - } - - /** - * Circular easing in - * @memberOf fabric.util.ease - */ - function easeInCirc(t, b, c, d) { - return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; - } - - /** - * Circular easing out - * @memberOf fabric.util.ease - */ - function easeOutCirc(t, b, c, d) { - return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; - } - - /** - * Circular easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutCirc(t, b, c, d) { - t /= d / 2; - if (t < 1) { - return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; - } - return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; - } - - /** - * Elastic easing in - * @memberOf fabric.util.ease - */ - function easeInElastic(t, b, c, d) { - var s = 1.70158, p = 0, a = c; - if (t === 0) { - return b; - } - t /= d; - if (t === 1) { - return b + c; - } - if (!p) { - p = d * 0.3; - } - var opts = normalize(a, c, p, s); - return -elastic(opts, t, d) + b; - } - - /** - * Elastic easing out - * @memberOf fabric.util.ease - */ - function easeOutElastic(t, b, c, d) { - var s = 1.70158, p = 0, a = c; - if (t === 0) { - return b; - } - t /= d; - if (t === 1) { - return b + c; - } - if (!p) { - p = d * 0.3; - } - var opts = normalize(a, c, p, s); - return opts.a * Math.pow(2, -10 * t) * Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p) + opts.c + b; - } - - /** - * Elastic easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutElastic(t, b, c, d) { - var s = 1.70158, p = 0, a = c; - if (t === 0) { - return b; - } - t /= d / 2; - if (t === 2) { - return b + c; - } - if (!p) { - p = d * (0.3 * 1.5); - } - var opts = normalize(a, c, p, s); - if (t < 1) { - return -0.5 * elastic(opts, t, d) + b; - } - return opts.a * Math.pow(2, -10 * (t -= 1)) * - Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p) * 0.5 + opts.c + b; - } - - /** - * Backwards easing in - * @memberOf fabric.util.ease - */ - function easeInBack(t, b, c, d, s) { - if (s === undefined) { - s = 1.70158; - } - return c * (t /= d) * t * ((s + 1) * t - s) + b; - } - - /** - * Backwards easing out - * @memberOf fabric.util.ease - */ - function easeOutBack(t, b, c, d, s) { - if (s === undefined) { - s = 1.70158; - } - return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; - } - - /** - * Backwards easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutBack(t, b, c, d, s) { - if (s === undefined) { - s = 1.70158; - } - t /= d / 2; - if (t < 1) { - return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; - } - return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; - } - - /** - * Bouncing easing in - * @memberOf fabric.util.ease - */ - function easeInBounce(t, b, c, d) { - return c - easeOutBounce(d - t, 0, c, d) + b; - } - - /** - * Bouncing easing out - * @memberOf fabric.util.ease - */ - function easeOutBounce(t, b, c, d) { - if ((t /= d) < (1 / 2.75)) { - return c * (7.5625 * t * t) + b; - } - else if (t < (2 / 2.75)) { - return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; - } - else if (t < (2.5 / 2.75)) { - return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; - } - else { - return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; - } - } - - /** - * Bouncing easing in and out - * @memberOf fabric.util.ease - */ - function easeInOutBounce(t, b, c, d) { - if (t < d / 2) { - return easeInBounce(t * 2, 0, c, d) * 0.5 + b; - } - return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; - } - - /** - * Easing functions - * See Easing Equations by Robert Penner - * @namespace fabric.util.ease - */ - fabric.util.ease = { - - /** - * Quadratic easing in - * @memberOf fabric.util.ease - */ - easeInQuad: function (t, b, c, d) { - return c * (t /= d) * t + b; - }, - - /** - * Quadratic easing out - * @memberOf fabric.util.ease - */ - easeOutQuad: function (t, b, c, d) { - return -c * (t /= d) * (t - 2) + b; - }, - - /** - * Quadratic easing in and out - * @memberOf fabric.util.ease - */ - easeInOutQuad: function (t, b, c, d) { - t /= (d / 2); - if (t < 1) { - return c / 2 * t * t + b; - } - return -c / 2 * ((--t) * (t - 2) - 1) + b; - }, - - /** - * Cubic easing in - * @memberOf fabric.util.ease - */ - easeInCubic: function (t, b, c, d) { - return c * (t /= d) * t * t + b; - }, - - easeOutCubic: easeOutCubic, - easeInOutCubic: easeInOutCubic, - easeInQuart: easeInQuart, - easeOutQuart: easeOutQuart, - easeInOutQuart: easeInOutQuart, - easeInQuint: easeInQuint, - easeOutQuint: easeOutQuint, - easeInOutQuint: easeInOutQuint, - easeInSine: easeInSine, - easeOutSine: easeOutSine, - easeInOutSine: easeInOutSine, - easeInExpo: easeInExpo, - easeOutExpo: easeOutExpo, - easeInOutExpo: easeInOutExpo, - easeInCirc: easeInCirc, - easeOutCirc: easeOutCirc, - easeInOutCirc: easeInOutCirc, - easeInElastic: easeInElastic, - easeOutElastic: easeOutElastic, - easeInOutElastic: easeInOutElastic, - easeInBack: easeInBack, - easeOutBack: easeOutBack, - easeInOutBack: easeInOutBack, - easeInBounce: easeInBounce, - easeOutBounce: easeOutBounce, - easeInOutBounce: easeInOutBounce - }; - -})(); - - -(function (global) { - - 'use strict'; - - /** - * @name fabric - * @namespace - */ - - var fabric = global.fabric || (global.fabric = {}), - extend = fabric.util.object.extend, - clone = fabric.util.object.clone, - toFixed = fabric.util.toFixed, - parseUnit = fabric.util.parseUnit, - multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, - - svgValidTagNames = ['path', 'circle', 'polygon', 'polyline', 'ellipse', 'rect', 'line', - 'image', 'text'], - svgViewBoxElements = ['symbol', 'image', 'marker', 'pattern', 'view', 'svg'], - svgInvalidAncestors = ['pattern', 'defs', 'symbol', 'metadata', 'clipPath', 'mask', 'desc'], - svgValidParents = ['symbol', 'g', 'a', 'svg', 'clipPath', 'defs'], - - attributesMap = { - cx: 'left', - x: 'left', - r: 'radius', - cy: 'top', - y: 'top', - display: 'visible', - visibility: 'visible', - transform: 'transformMatrix', - 'fill-opacity': 'fillOpacity', - 'fill-rule': 'fillRule', - 'font-family': 'fontFamily', - 'font-size': 'fontSize', - 'font-style': 'fontStyle', - 'font-weight': 'fontWeight', - 'letter-spacing': 'charSpacing', - 'paint-order': 'paintFirst', - 'stroke-dasharray': 'strokeDashArray', - 'stroke-dashoffset': 'strokeDashOffset', - 'stroke-linecap': 'strokeLineCap', - 'stroke-linejoin': 'strokeLineJoin', - 'stroke-miterlimit': 'strokeMiterLimit', - 'stroke-opacity': 'strokeOpacity', - 'stroke-width': 'strokeWidth', - 'text-decoration': 'textDecoration', - 'text-anchor': 'textAnchor', - opacity: 'opacity', - 'clip-path': 'clipPath', - 'clip-rule': 'clipRule', - 'vector-effect': 'strokeUniform', - 'image-rendering': 'imageSmoothing', - }, - - colorAttributes = { - stroke: 'strokeOpacity', - fill: 'fillOpacity' - }, - - fSize = 'font-size', cPath = 'clip-path'; - - fabric.svgValidTagNamesRegEx = getSvgRegex(svgValidTagNames); - fabric.svgViewBoxElementsRegEx = getSvgRegex(svgViewBoxElements); - fabric.svgInvalidAncestorsRegEx = getSvgRegex(svgInvalidAncestors); - fabric.svgValidParentsRegEx = getSvgRegex(svgValidParents); - - fabric.cssRules = {}; - fabric.gradientDefs = {}; - fabric.clipPaths = {}; - - function normalizeAttr(attr) { - // transform attribute names - if (attr in attributesMap) { - return attributesMap[attr]; - } - return attr; - } - - function normalizeValue(attr, value, parentAttributes, fontSize) { - var isArray = Array.isArray(value), parsed; - - if ((attr === 'fill' || attr === 'stroke') && value === 'none') { - value = ''; - } - else if (attr === 'strokeUniform') { - return (value === 'non-scaling-stroke'); - } - else if (attr === 'strokeDashArray') { - if (value === 'none') { - value = null; - } - else { - value = value.replace(/,/g, ' ').split(/\s+/).map(parseFloat); - } - } - else if (attr === 'transformMatrix') { - if (parentAttributes && parentAttributes.transformMatrix) { - value = multiplyTransformMatrices( - parentAttributes.transformMatrix, fabric.parseTransformAttribute(value)); - } - else { - value = fabric.parseTransformAttribute(value); - } - } - else if (attr === 'visible') { - value = value !== 'none' && value !== 'hidden'; - // display=none on parent element always takes precedence over child element - if (parentAttributes && parentAttributes.visible === false) { - value = false; - } - } - else if (attr === 'opacity') { - value = parseFloat(value); - if (parentAttributes && typeof parentAttributes.opacity !== 'undefined') { - value *= parentAttributes.opacity; - } - } - else if (attr === 'textAnchor' /* text-anchor */) { - value = value === 'start' ? 'left' : value === 'end' ? 'right' : 'center'; - } - else if (attr === 'charSpacing') { - // parseUnit returns px and we convert it to em - parsed = parseUnit(value, fontSize) / fontSize * 1000; - } - else if (attr === 'paintFirst') { - var fillIndex = value.indexOf('fill'); - var strokeIndex = value.indexOf('stroke'); - var value = 'fill'; - if (fillIndex > -1 && strokeIndex > -1 && strokeIndex < fillIndex) { - value = 'stroke'; - } - else if (fillIndex === -1 && strokeIndex > -1) { - value = 'stroke'; - } - } - else if (attr === 'href' || attr === 'xlink:href' || attr === 'font') { - return value; - } - else if (attr === 'imageSmoothing') { - return (value === 'optimizeQuality'); - } - else { - parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize); - } - - return (!isArray && isNaN(parsed) ? value : parsed); - } - - /** - * @private - */ - function getSvgRegex(arr) { - return new RegExp('^(' + arr.join('|') + ')\\b', 'i'); - } - - /** - * @private - * @param {Object} attributes Array of attributes to parse - */ - function _setStrokeFillOpacity(attributes) { - for (var attr in colorAttributes) { - - if (typeof attributes[colorAttributes[attr]] === 'undefined' || attributes[attr] === '') { - continue; - } - - if (typeof attributes[attr] === 'undefined') { - if (!fabric.Object.prototype[attr]) { - continue; - } - attributes[attr] = fabric.Object.prototype[attr]; - } - - if (attributes[attr].indexOf('url(') === 0) { - continue; - } - - var color = new fabric.Color(attributes[attr]); - attributes[attr] = color.setAlpha(toFixed(color.getAlpha() * attributes[colorAttributes[attr]], 2)).toRgba(); - } - return attributes; - } - - /** - * @private - */ - function _getMultipleNodes(doc, nodeNames) { - var nodeName, nodeArray = [], nodeList, i, len; - for (i = 0, len = nodeNames.length; i < len; i++) { - nodeName = nodeNames[i]; - nodeList = doc.getElementsByTagName(nodeName); - nodeArray = nodeArray.concat(Array.prototype.slice.call(nodeList)); - } - return nodeArray; - } - - /** - * Parses "transform" attribute, returning an array of values - * @static - * @function - * @memberOf fabric - * @param {String} attributeValue String containing attribute value - * @return {Array} Array of 6 elements representing transformation matrix - */ - fabric.parseTransformAttribute = (function () { - function rotateMatrix(matrix, args) { - var cos = fabric.util.cos(args[0]), sin = fabric.util.sin(args[0]), - x = 0, y = 0; - if (args.length === 3) { - x = args[1]; - y = args[2]; - } - - matrix[0] = cos; - matrix[1] = sin; - matrix[2] = -sin; - matrix[3] = cos; - matrix[4] = x - (cos * x - sin * y); - matrix[5] = y - (sin * x + cos * y); - } - - function scaleMatrix(matrix, args) { - var multiplierX = args[0], - multiplierY = (args.length === 2) ? args[1] : args[0]; - - matrix[0] = multiplierX; - matrix[3] = multiplierY; - } - - function skewMatrix(matrix, args, pos) { - matrix[pos] = Math.tan(fabric.util.degreesToRadians(args[0])); - } - - function translateMatrix(matrix, args) { - matrix[4] = args[0]; - if (args.length === 2) { - matrix[5] = args[1]; - } - } - - // identity matrix - var iMatrix = fabric.iMatrix, - - // == begin transform regexp - number = fabric.reNum, - - commaWsp = fabric.commaWsp, - - skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', - - skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', - - rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + - commaWsp + '(' + number + ')' + - commaWsp + '(' + number + '))?\\s*\\))', - - scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + - commaWsp + '(' + number + '))?\\s*\\))', - - translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + - commaWsp + '(' + number + '))?\\s*\\))', - - matrix = '(?:(matrix)\\s*\\(\\s*' + - '(' + number + ')' + commaWsp + - '(' + number + ')' + commaWsp + - '(' + number + ')' + commaWsp + - '(' + number + ')' + commaWsp + - '(' + number + ')' + commaWsp + - '(' + number + ')' + - '\\s*\\))', - - transform = '(?:' + - matrix + '|' + - translate + '|' + - scale + '|' + - rotate + '|' + - skewX + '|' + - skewY + - ')', - - transforms = '(?:' + transform + '(?:' + commaWsp + '*' + transform + ')*' + ')', - - transformList = '^\\s*(?:' + transforms + '?)\\s*$', - - // http://www.w3.org/TR/SVG/coords.html#TransformAttribute - reTransformList = new RegExp(transformList), - // == end transform regexp - - reTransform = new RegExp(transform, 'g'); - - return function (attributeValue) { - - // start with identity matrix - var matrix = iMatrix.concat(), - matrices = []; - - // return if no argument was given or - // an argument does not match transform attribute regexp - if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) { - return matrix; - } - - attributeValue.replace(reTransform, function (match) { - - var m = new RegExp(transform).exec(match).filter(function (match) { - // match !== '' && match != null - return (!!match); - }), - operation = m[1], - args = m.slice(2).map(parseFloat); - - switch (operation) { - case 'translate': - translateMatrix(matrix, args); - break; - case 'rotate': - args[0] = fabric.util.degreesToRadians(args[0]); - rotateMatrix(matrix, args); - break; - case 'scale': - scaleMatrix(matrix, args); - break; - case 'skewX': - skewMatrix(matrix, args, 2); - break; - case 'skewY': - skewMatrix(matrix, args, 1); - break; - case 'matrix': - matrix = args; - break; - } - - // snapshot current matrix into matrices array - matrices.push(matrix.concat()); - // reset - matrix = iMatrix.concat(); - }); - - var combinedMatrix = matrices[0]; - while (matrices.length > 1) { - matrices.shift(); - combinedMatrix = fabric.util.multiplyTransformMatrices(combinedMatrix, matrices[0]); - } - return combinedMatrix; - }; - })(); - - /** - * @private - */ - function parseStyleString(style, oStyle) { - var attr, value; - style.replace(/;\s*$/, '').split(';').forEach(function (chunk) { - var pair = chunk.split(':'); - - attr = pair[0].trim().toLowerCase(); - value = pair[1].trim(); - - oStyle[attr] = value; - }); - } - - /** - * @private - */ - function parseStyleObject(style, oStyle) { - var attr, value; - for (var prop in style) { - if (typeof style[prop] === 'undefined') { - continue; - } - - attr = prop.toLowerCase(); - value = style[prop]; - - oStyle[attr] = value; - } - } - - /** - * @private - */ - function getGlobalStylesForElement(element, svgUid) { - var styles = {}; - for (var rule in fabric.cssRules[svgUid]) { - if (elementMatchesRule(element, rule.split(' '))) { - for (var property in fabric.cssRules[svgUid][rule]) { - styles[property] = fabric.cssRules[svgUid][rule][property]; - } - } - } - return styles; - } - - /** - * @private - */ - function elementMatchesRule(element, selectors) { - var firstMatching, parentMatching = true; - //start from rightmost selector. - firstMatching = selectorMatches(element, selectors.pop()); - if (firstMatching && selectors.length) { - parentMatching = doesSomeParentMatch(element, selectors); - } - return firstMatching && parentMatching && (selectors.length === 0); - } - - function doesSomeParentMatch(element, selectors) { - var selector, parentMatching = true; - while (element.parentNode && element.parentNode.nodeType === 1 && selectors.length) { - if (parentMatching) { - selector = selectors.pop(); - } - element = element.parentNode; - parentMatching = selectorMatches(element, selector); - } - return selectors.length === 0; - } - - /** - * @private - */ - function selectorMatches(element, selector) { - var nodeName = element.nodeName, - classNames = element.getAttribute('class'), - id = element.getAttribute('id'), matcher, i; - // i check if a selector matches slicing away part from it. - // if i get empty string i should match - matcher = new RegExp('^' + nodeName, 'i'); - selector = selector.replace(matcher, ''); - if (id && selector.length) { - matcher = new RegExp('#' + id + '(?![a-zA-Z\\-]+)', 'i'); - selector = selector.replace(matcher, ''); - } - if (classNames && selector.length) { - classNames = classNames.split(' '); - for (i = classNames.length; i--;) { - matcher = new RegExp('\\.' + classNames[i] + '(?![a-zA-Z\\-]+)', 'i'); - selector = selector.replace(matcher, ''); - } - } - return selector.length === 0; - } - - /** - * @private - * to support IE8 missing getElementById on SVGdocument and on node xmlDOM - */ - function elementById(doc, id) { - var el; - doc.getElementById && (el = doc.getElementById(id)); - if (el) { - return el; - } - var node, i, len, nodelist = doc.getElementsByTagName('*'); - for (i = 0, len = nodelist.length; i < len; i++) { - node = nodelist[i]; - if (id === node.getAttribute('id')) { - return node; - } - } - } - - /** - * @private - */ - function parseUseDirectives(doc) { - var nodelist = _getMultipleNodes(doc, ['use', 'svg:use']), i = 0; - while (nodelist.length && i < nodelist.length) { - var el = nodelist[i], - xlinkAttribute = el.getAttribute('xlink:href') || el.getAttribute('href'); - - if (xlinkAttribute === null) { - return; - } - - var xlink = xlinkAttribute.slice(1), - x = el.getAttribute('x') || 0, - y = el.getAttribute('y') || 0, - el2 = elementById(doc, xlink).cloneNode(true), - currentTrans = (el2.getAttribute('transform') || '') + ' translate(' + x + ', ' + y + ')', - parentNode, - oldLength = nodelist.length, attr, - j, - attrs, - len, - namespace = fabric.svgNS; - - applyViewboxTransform(el2); - if (/^svg$/i.test(el2.nodeName)) { - var el3 = el2.ownerDocument.createElementNS(namespace, 'g'); - for (j = 0, attrs = el2.attributes, len = attrs.length; j < len; j++) { - attr = attrs.item(j); - el3.setAttributeNS(namespace, attr.nodeName, attr.nodeValue); - } - // el2.firstChild != null - while (el2.firstChild) { - el3.appendChild(el2.firstChild); - } - el2 = el3; - } - - for (j = 0, attrs = el.attributes, len = attrs.length; j < len; j++) { - attr = attrs.item(j); - if (attr.nodeName === 'x' || attr.nodeName === 'y' || - attr.nodeName === 'xlink:href' || attr.nodeName === 'href') { - continue; - } - - if (attr.nodeName === 'transform') { - currentTrans = attr.nodeValue + ' ' + currentTrans; - } - else { - el2.setAttribute(attr.nodeName, attr.nodeValue); - } - } - - el2.setAttribute('transform', currentTrans); - el2.setAttribute('instantiated_by_use', '1'); - el2.removeAttribute('id'); - parentNode = el.parentNode; - parentNode.replaceChild(el2, el); - // some browsers do not shorten nodelist after replaceChild (IE8) - if (nodelist.length === oldLength) { - i++; - } - } - } - - // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute - // matches, e.g.: +14.56e-12, etc. - var reViewBoxAttrValue = new RegExp( - '^' + - '\\s*(' + fabric.reNum + '+)\\s*,?' + - '\\s*(' + fabric.reNum + '+)\\s*,?' + - '\\s*(' + fabric.reNum + '+)\\s*,?' + - '\\s*(' + fabric.reNum + '+)\\s*' + - '$' - ); - - /** - * Add a element that envelop all child elements and makes the viewbox transformMatrix descend on all elements - */ - function applyViewboxTransform(element) { - if (!fabric.svgViewBoxElementsRegEx.test(element.nodeName)) { - return {}; - } - var viewBoxAttr = element.getAttribute('viewBox'), - scaleX = 1, - scaleY = 1, - minX = 0, - minY = 0, - viewBoxWidth, viewBoxHeight, matrix, el, - widthAttr = element.getAttribute('width'), - heightAttr = element.getAttribute('height'), - x = element.getAttribute('x') || 0, - y = element.getAttribute('y') || 0, - preserveAspectRatio = element.getAttribute('preserveAspectRatio') || '', - missingViewBox = (!viewBoxAttr || !(viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))), - missingDimAttr = (!widthAttr || !heightAttr || widthAttr === '100%' || heightAttr === '100%'), - toBeParsed = missingViewBox && missingDimAttr, - parsedDim = {}, translateMatrix = '', widthDiff = 0, heightDiff = 0; - - parsedDim.width = 0; - parsedDim.height = 0; - parsedDim.toBeParsed = toBeParsed; - - if (missingViewBox) { - if (((x || y) && element.parentNode && element.parentNode.nodeName !== '#document')) { - translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; - matrix = (element.getAttribute('transform') || '') + translateMatrix; - element.setAttribute('transform', matrix); - element.removeAttribute('x'); - element.removeAttribute('y'); - } - } - - if (toBeParsed) { - return parsedDim; - } - - if (missingViewBox) { - parsedDim.width = parseUnit(widthAttr); - parsedDim.height = parseUnit(heightAttr); - // set a transform for elements that have x y and are inner(only) SVGs - return parsedDim; - } - minX = -parseFloat(viewBoxAttr[1]); - minY = -parseFloat(viewBoxAttr[2]); - viewBoxWidth = parseFloat(viewBoxAttr[3]); - viewBoxHeight = parseFloat(viewBoxAttr[4]); - parsedDim.minX = minX; - parsedDim.minY = minY; - parsedDim.viewBoxWidth = viewBoxWidth; - parsedDim.viewBoxHeight = viewBoxHeight; - if (!missingDimAttr) { - parsedDim.width = parseUnit(widthAttr); - parsedDim.height = parseUnit(heightAttr); - scaleX = parsedDim.width / viewBoxWidth; - scaleY = parsedDim.height / viewBoxHeight; - } - else { - parsedDim.width = viewBoxWidth; - parsedDim.height = viewBoxHeight; - } - - // default is to preserve aspect ratio - preserveAspectRatio = fabric.util.parsePreserveAspectRatioAttribute(preserveAspectRatio); - if (preserveAspectRatio.alignX !== 'none') { - //translate all container for the effect of Mid, Min, Max - if (preserveAspectRatio.meetOrSlice === 'meet') { - scaleY = scaleX = (scaleX > scaleY ? scaleY : scaleX); - // calculate additional translation to move the viewbox - } - if (preserveAspectRatio.meetOrSlice === 'slice') { - scaleY = scaleX = (scaleX > scaleY ? scaleX : scaleY); - // calculate additional translation to move the viewbox - } - widthDiff = parsedDim.width - viewBoxWidth * scaleX; - heightDiff = parsedDim.height - viewBoxHeight * scaleX; - if (preserveAspectRatio.alignX === 'Mid') { - widthDiff /= 2; - } - if (preserveAspectRatio.alignY === 'Mid') { - heightDiff /= 2; - } - if (preserveAspectRatio.alignX === 'Min') { - widthDiff = 0; - } - if (preserveAspectRatio.alignY === 'Min') { - heightDiff = 0; - } - } - - if (scaleX === 1 && scaleY === 1 && minX === 0 && minY === 0 && x === 0 && y === 0) { - return parsedDim; - } - if ((x || y) && element.parentNode.nodeName !== '#document') { - translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; - } - - matrix = translateMatrix + ' matrix(' + scaleX + - ' 0' + - ' 0 ' + - scaleY + ' ' + - (minX * scaleX + widthDiff) + ' ' + - (minY * scaleY + heightDiff) + ') '; - // seems unused. - // parsedDim.viewboxTransform = fabric.parseTransformAttribute(matrix); - if (element.nodeName === 'svg') { - el = element.ownerDocument.createElementNS(fabric.svgNS, 'g'); - // element.firstChild != null - while (element.firstChild) { - el.appendChild(element.firstChild); - } - element.appendChild(el); - } - else { - el = element; - el.removeAttribute('x'); - el.removeAttribute('y'); - matrix = el.getAttribute('transform') + matrix; - } - el.setAttribute('transform', matrix); - return parsedDim; - } - - function hasAncestorWithNodeName(element, nodeName) { - while (element && (element = element.parentNode)) { - if (element.nodeName && nodeName.test(element.nodeName.replace('svg:', '')) - && !element.getAttribute('instantiated_by_use')) { - return true; - } - } - return false; - } - - /** - * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback - * @static - * @function - * @memberOf fabric - * @param {SVGDocument} doc SVG document to parse - * @param {Function} callback Callback to call when parsing is finished; - * It's being passed an array of elements (parsed from a document). - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - * @param {Object} [parsingOptions] options for parsing document - * @param {String} [parsingOptions.crossOrigin] crossOrigin settings - */ - fabric.parseSVGDocument = function (doc, callback, reviver, parsingOptions) { - if (!doc) { - return; - } - - parseUseDirectives(doc); - - var svgUid = fabric.Object.__uid++, i, len, - options = applyViewboxTransform(doc), - descendants = fabric.util.toArray(doc.getElementsByTagName('*')); - options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; - options.svgUid = svgUid; - - if (descendants.length === 0 && fabric.isLikelyNode) { - // we're likely in node, where "o3-xml" library fails to gEBTN("*") - // https://github.com/ajaxorg/node-o3-xml/issues/21 - descendants = doc.selectNodes('//*[name(.)!="svg"]'); - var arr = []; - for (i = 0, len = descendants.length; i < len; i++) { - arr[i] = descendants[i]; - } - descendants = arr; - } - - var elements = descendants.filter(function (el) { - applyViewboxTransform(el); - return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')) && - !hasAncestorWithNodeName(el, fabric.svgInvalidAncestorsRegEx); // http://www.w3.org/TR/SVG/struct.html#DefsElement - }); - if (!elements || (elements && !elements.length)) { - callback && callback([], {}); - return; - } - var clipPaths = {}; - descendants.filter(function (el) { - return el.nodeName.replace('svg:', '') === 'clipPath'; - }).forEach(function (el) { - var id = el.getAttribute('id'); - clipPaths[id] = fabric.util.toArray(el.getElementsByTagName('*')).filter(function (el) { - return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')); - }); - }); - fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); - fabric.cssRules[svgUid] = fabric.getCSSRules(doc); - fabric.clipPaths[svgUid] = clipPaths; - // Precedence of rules: style > class > attribute - fabric.parseElements(elements, function (instances, elements) { - if (callback) { - callback(instances, options, elements, descendants); - delete fabric.gradientDefs[svgUid]; - delete fabric.cssRules[svgUid]; - delete fabric.clipPaths[svgUid]; - } - }, clone(options), reviver, parsingOptions); - }; - - function recursivelyParseGradientsXlink(doc, gradient) { - var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], - xlinkAttr = 'xlink:href', - xLink = gradient.getAttribute(xlinkAttr).slice(1), - referencedGradient = elementById(doc, xLink); - if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { - recursivelyParseGradientsXlink(doc, referencedGradient); - } - gradientsAttrs.forEach(function (attr) { - if (referencedGradient && !gradient.hasAttribute(attr) && referencedGradient.hasAttribute(attr)) { - gradient.setAttribute(attr, referencedGradient.getAttribute(attr)); - } - }); - if (!gradient.children.length) { - var referenceClone = referencedGradient.cloneNode(true); - while (referenceClone.firstChild) { - gradient.appendChild(referenceClone.firstChild); - } - } - gradient.removeAttribute(xlinkAttr); - } - - var reFontDeclaration = new RegExp( - '(normal|italic)?\\s*(normal|small-caps)?\\s*' + - '(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*(' + - fabric.reNum + - '(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|' + fabric.reNum + '))?\\s+(.*)'); - - extend(fabric, { - /** - * Parses a short font declaration, building adding its properties to a style object - * @static - * @function - * @memberOf fabric - * @param {String} value font declaration - * @param {Object} oStyle definition - */ - parseFontDeclaration: function (value, oStyle) { - var match = value.match(reFontDeclaration); - - if (!match) { - return; - } - var fontStyle = match[1], - // font variant is not used - // fontVariant = match[2], - fontWeight = match[3], - fontSize = match[4], - lineHeight = match[5], - fontFamily = match[6]; - - if (fontStyle) { - oStyle.fontStyle = fontStyle; - } - if (fontWeight) { - oStyle.fontWeight = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight); - } - if (fontSize) { - oStyle.fontSize = parseUnit(fontSize); - } - if (fontFamily) { - oStyle.fontFamily = fontFamily; - } - if (lineHeight) { - oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight; - } - }, - - /** - * Parses an SVG document, returning all of the gradient declarations found in it - * @static - * @function - * @memberOf fabric - * @param {SVGDocument} doc SVG document to parse - * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element - */ - getGradientDefs: function (doc) { - var tagArray = [ - 'linearGradient', - 'radialGradient', - 'svg:linearGradient', - 'svg:radialGradient'], - elList = _getMultipleNodes(doc, tagArray), - el, j = 0, gradientDefs = {}; - j = elList.length; - while (j--) { - el = elList[j]; - if (el.getAttribute('xlink:href')) { - recursivelyParseGradientsXlink(doc, el); - } - gradientDefs[el.getAttribute('id')] = el; - } - return gradientDefs; - }, - - /** - * Returns an object of attributes' name/value, given element and an array of attribute names; - * Parses parent "g" nodes recursively upwards. - * @static - * @memberOf fabric - * @param {DOMElement} element Element to parse - * @param {Array} attributes Array of attributes to parse - * @return {Object} object containing parsed attributes' names/values - */ - parseAttributes: function (element, attributes, svgUid) { - - if (!element) { - return; - } - - var value, - parentAttributes = {}, - fontSize, parentFontSize; - - if (typeof svgUid === 'undefined') { - svgUid = element.getAttribute('svgUid'); - } - // if there's a parent container (`g` or `a` or `symbol` node), parse its attributes recursively upwards - if (element.parentNode && fabric.svgValidParentsRegEx.test(element.parentNode.nodeName)) { - parentAttributes = fabric.parseAttributes(element.parentNode, attributes, svgUid); - } - - var ownAttributes = attributes.reduce(function (memo, attr) { - value = element.getAttribute(attr); - if (value) { // eslint-disable-line - memo[attr] = value; - } - return memo; - }, {}); - // add values parsed from style, which take precedence over attributes - // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) - var cssAttrs = extend( - getGlobalStylesForElement(element, svgUid), - fabric.parseStyleAttribute(element) - ); - ownAttributes = extend( - ownAttributes, - cssAttrs - ); - if (cssAttrs[cPath]) { - element.setAttribute(cPath, cssAttrs[cPath]); - } - fontSize = parentFontSize = parentAttributes.fontSize || fabric.Text.DEFAULT_SVG_FONT_SIZE; - if (ownAttributes[fSize]) { - // looks like the minimum should be 9px when dealing with ems. this is what looks like in browsers. - ownAttributes[fSize] = fontSize = parseUnit(ownAttributes[fSize], parentFontSize); - } - - var normalizedAttr, normalizedValue, normalizedStyle = {}; - for (var attr in ownAttributes) { - normalizedAttr = normalizeAttr(attr); - normalizedValue = normalizeValue(normalizedAttr, ownAttributes[attr], parentAttributes, fontSize); - normalizedStyle[normalizedAttr] = normalizedValue; - } - if (normalizedStyle && normalizedStyle.font) { - fabric.parseFontDeclaration(normalizedStyle.font, normalizedStyle); - } - var mergedAttrs = extend(parentAttributes, normalizedStyle); - return fabric.svgValidParentsRegEx.test(element.nodeName) ? mergedAttrs : _setStrokeFillOpacity(mergedAttrs); - }, - - /** - * Transforms an array of svg elements to corresponding fabric.* instances - * @static - * @memberOf fabric - * @param {Array} elements Array of elements to parse - * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) - * @param {Object} [options] Options object - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - parseElements: function (elements, callback, options, reviver, parsingOptions) { - new fabric.ElementsParser(elements, callback, options, reviver, parsingOptions).parse(); - }, - - /** - * Parses "style" attribute, retuning an object with values - * @static - * @memberOf fabric - * @param {SVGElement} element Element to parse - * @return {Object} Objects with values parsed from style attribute of an element - */ - parseStyleAttribute: function (element) { - var oStyle = {}, - style = element.getAttribute('style'); - - if (!style) { - return oStyle; - } - - if (typeof style === 'string') { - parseStyleString(style, oStyle); - } - else { - parseStyleObject(style, oStyle); - } - - return oStyle; - }, - - /** - * Parses "points" attribute, returning an array of values - * @static - * @memberOf fabric - * @param {String} points points attribute string - * @return {Array} array of points - */ - parsePointsAttribute: function (points) { - - // points attribute is required and must not be empty - if (!points) { - return null; - } - - // replace commas with whitespace and remove bookending whitespace - points = points.replace(/,/g, ' ').trim(); - - points = points.split(/\s+/); - var parsedPoints = [], i, len; - - for (i = 0, len = points.length; i < len; i += 2) { - parsedPoints.push({ - x: parseFloat(points[i]), - y: parseFloat(points[i + 1]) - }); - } - - // odd number of points is an error - // if (parsedPoints.length % 2 !== 0) { - // return null; - // } - - return parsedPoints; - }, - - /** - * Returns CSS rules for a given SVG document - * @static - * @function - * @memberOf fabric - * @param {SVGDocument} doc SVG document to parse - * @return {Object} CSS rules of this document - */ - getCSSRules: function (doc) { - var styles = doc.getElementsByTagName('style'), i, len, - allRules = {}, rules; - - // very crude parsing of style contents - for (i = 0, len = styles.length; i < len; i++) { - var styleContents = styles[i].textContent; - - // remove comments - styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); - if (styleContents.trim() === '') { - continue; - } - // recovers all the rule in this form `body { style code... }` - // rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); - rules = styleContents.split('}'); - // remove empty rules. - rules = rules.filter(function (rule) { return rule.trim(); }); - // at this point we have hopefully an array of rules `body { style code... ` - // eslint-disable-next-line no-loop-func - rules.forEach(function (rule) { - - var match = rule.split('{'), - ruleObj = {}, declaration = match[1].trim(), - propertyValuePairs = declaration.split(';').filter(function (pair) { return pair.trim(); }); - - for (i = 0, len = propertyValuePairs.length; i < len; i++) { - var pair = propertyValuePairs[i].split(':'), - property = pair[0].trim(), - value = pair[1].trim(); - ruleObj[property] = value; - } - rule = match[0].trim(); - rule.split(',').forEach(function (_rule) { - _rule = _rule.replace(/^svg/i, '').trim(); - if (_rule === '') { - return; - } - if (allRules[_rule]) { - fabric.util.object.extend(allRules[_rule], ruleObj); - } - else { - allRules[_rule] = fabric.util.object.clone(ruleObj); - } - }); - }); - } - return allRules; - }, - - /** - * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. - * Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) - * @memberOf fabric - * @param {String} url - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - * @param {Object} [options] Object containing options for parsing - * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources - */ - loadSVGFromURL: function (url, callback, reviver, options) { - - url = url.replace(/^\n\s*/, '').trim(); - new fabric.util.request(url, { - method: 'get', - onComplete: onComplete - }); - - function onComplete(r) { - - var xml = r.responseXML; - if (!xml || !xml.documentElement) { - callback && callback(null); - return false; - } - - fabric.parseSVGDocument(xml.documentElement, function (results, _options, elements, allElements) { - callback && callback(results, _options, elements, allElements); - }, reviver, options); - } - }, - - /** - * Takes string corresponding to an SVG document, and parses it into a set of fabric objects - * @memberOf fabric - * @param {String} string - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - * @param {Object} [options] Object containing options for parsing - * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources - */ - loadSVGFromString: function (string, callback, reviver, options) { - var parser = new fabric.window.DOMParser(), - doc = parser.parseFromString(string.trim(), 'text/xml'); - fabric.parseSVGDocument(doc.documentElement, function (results, _options, elements, allElements) { - callback(results, _options, elements, allElements); - }, reviver, options); - } - }); - -})(typeof exports !== 'undefined' ? exports : this); - - -fabric.ElementsParser = function (elements, callback, options, reviver, parsingOptions, doc) { - this.elements = elements; - this.callback = callback; - this.options = options; - this.reviver = reviver; - this.svgUid = (options && options.svgUid) || 0; - this.parsingOptions = parsingOptions; - this.regexUrl = /^url\(['"]?#([^'"]+)['"]?\)/g; - this.doc = doc; -}; - -(function (proto) { - proto.parse = function () { - this.instances = new Array(this.elements.length); - this.numElements = this.elements.length; - this.createObjects(); - }; - - proto.createObjects = function () { - var _this = this; - this.elements.forEach(function (element, i) { - element.setAttribute('svgUid', _this.svgUid); - _this.createObject(element, i); - }); - }; - - proto.findTag = function (el) { - return fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))]; - }; - - proto.createObject = function (el, index) { - var klass = this.findTag(el); - if (klass && klass.fromElement) { - try { - klass.fromElement(el, this.createCallback(index, el), this.options); - } - catch (err) { - fabric.log(err); - } - } - else { - this.checkIfDone(); - } - }; - - proto.createCallback = function (index, el) { - var _this = this; - return function (obj) { - var _options; - _this.resolveGradient(obj, el, 'fill'); - _this.resolveGradient(obj, el, 'stroke'); - if (obj instanceof fabric.Image && obj._originalElement) { - _options = obj.parsePreserveAspectRatioAttribute(el); - } - obj._removeTransformMatrix(_options); - _this.resolveClipPath(obj, el); - _this.reviver && _this.reviver(el, obj); - _this.instances[index] = obj; - _this.checkIfDone(); - }; - }; - - proto.extractPropertyDefinition = function (obj, property, storage) { - var value = obj[property], regex = this.regexUrl; - if (!regex.test(value)) { - return; - } - regex.lastIndex = 0; - var id = regex.exec(value)[1]; - regex.lastIndex = 0; - return fabric[storage][this.svgUid][id]; - }; - - proto.resolveGradient = function (obj, el, property) { - var gradientDef = this.extractPropertyDefinition(obj, property, 'gradientDefs'); - if (gradientDef) { - var opacityAttr = el.getAttribute(property + '-opacity'); - var gradient = fabric.Gradient.fromElement(gradientDef, obj, opacityAttr, this.options); - obj.set(property, gradient); - } - }; - - proto.createClipPathCallback = function (obj, container) { - return function (_newObj) { - _newObj._removeTransformMatrix(); - _newObj.fillRule = _newObj.clipRule; - container.push(_newObj); - }; - }; - - proto.resolveClipPath = function (obj, usingElement) { - var clipPath = this.extractPropertyDefinition(obj, 'clipPath', 'clipPaths'), - element, klass, objTransformInv, container, gTransform, options; - if (clipPath) { - container = []; - objTransformInv = fabric.util.invertTransform(obj.calcTransformMatrix()); - // move the clipPath tag as sibling to the real element that is using it - var clipPathTag = clipPath[0].parentNode; - var clipPathOwner = usingElement; - while (clipPathOwner.parentNode && clipPathOwner.getAttribute('clip-path') !== obj.clipPath) { - clipPathOwner = clipPathOwner.parentNode; - } - clipPathOwner.parentNode.appendChild(clipPathTag); - for (var i = 0; i < clipPath.length; i++) { - element = clipPath[i]; - klass = this.findTag(element); - klass.fromElement( - element, - this.createClipPathCallback(obj, container), - this.options - ); - } - if (container.length === 1) { - clipPath = container[0]; - } - else { - clipPath = new fabric.Group(container); - } - gTransform = fabric.util.multiplyTransformMatrices( - objTransformInv, - clipPath.calcTransformMatrix() - ); - if (clipPath.clipPath) { - this.resolveClipPath(clipPath, clipPathOwner); - } - var options = fabric.util.qrDecompose(gTransform); - clipPath.flipX = false; - clipPath.flipY = false; - clipPath.set('scaleX', options.scaleX); - clipPath.set('scaleY', options.scaleY); - clipPath.angle = options.angle; - clipPath.skewX = options.skewX; - clipPath.skewY = 0; - clipPath.setPositionByOrigin({ x: options.translateX, y: options.translateY }, 'center', 'center'); - obj.clipPath = clipPath; - } - else { - // if clip-path does not resolve to any element, delete the property. - delete obj.clipPath; - } - }; - - proto.checkIfDone = function () { - if (--this.numElements === 0) { - this.instances = this.instances.filter(function (el) { - // eslint-disable-next-line no-eq-null, eqeqeq - return el != null; - }); - this.callback(this.instances, this.elements); - } - }; -})(fabric.ElementsParser.prototype); - - -(function (global) { - - 'use strict'; - - /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ - - var fabric = global.fabric || (global.fabric = {}); - - if (fabric.Point) { - fabric.warn('fabric.Point is already defined'); - return; - } - - fabric.Point = Point; - - /** - * Point class - * @class fabric.Point - * @memberOf fabric - * @constructor - * @param {Number} x - * @param {Number} y - * @return {fabric.Point} thisArg - */ - function Point(x, y) { - this.x = x; - this.y = y; - } - - Point.prototype = /** @lends fabric.Point.prototype */ { - - type: 'point', - - constructor: Point, - - /** - * Adds another point to this one and returns another one - * @param {fabric.Point} that - * @return {fabric.Point} new Point instance with added values - */ - add: function (that) { - return new Point(this.x + that.x, this.y + that.y); - }, - - /** - * Adds another point to this one - * @param {fabric.Point} that - * @return {fabric.Point} thisArg - * @chainable - */ - addEquals: function (that) { - this.x += that.x; - this.y += that.y; - return this; - }, - - /** - * Adds value to this point and returns a new one - * @param {Number} scalar - * @return {fabric.Point} new Point with added value - */ - scalarAdd: function (scalar) { - return new Point(this.x + scalar, this.y + scalar); - }, - - /** - * Adds value to this point - * @param {Number} scalar - * @return {fabric.Point} thisArg - * @chainable - */ - scalarAddEquals: function (scalar) { - this.x += scalar; - this.y += scalar; - return this; - }, - - /** - * Subtracts another point from this point and returns a new one - * @param {fabric.Point} that - * @return {fabric.Point} new Point object with subtracted values - */ - subtract: function (that) { - return new Point(this.x - that.x, this.y - that.y); - }, - - /** - * Subtracts another point from this point - * @param {fabric.Point} that - * @return {fabric.Point} thisArg - * @chainable - */ - subtractEquals: function (that) { - this.x -= that.x; - this.y -= that.y; - return this; - }, - - /** - * Subtracts value from this point and returns a new one - * @param {Number} scalar - * @return {fabric.Point} - */ - scalarSubtract: function (scalar) { - return new Point(this.x - scalar, this.y - scalar); - }, - - /** - * Subtracts value from this point - * @param {Number} scalar - * @return {fabric.Point} thisArg - * @chainable - */ - scalarSubtractEquals: function (scalar) { - this.x -= scalar; - this.y -= scalar; - return this; - }, - - /** - * Multiplies this point by a value and returns a new one - * TODO: rename in scalarMultiply in 2.0 - * @param {Number} scalar - * @return {fabric.Point} - */ - multiply: function (scalar) { - return new Point(this.x * scalar, this.y * scalar); - }, - - /** - * Multiplies this point by a value - * TODO: rename in scalarMultiplyEquals in 2.0 - * @param {Number} scalar - * @return {fabric.Point} thisArg - * @chainable - */ - multiplyEquals: function (scalar) { - this.x *= scalar; - this.y *= scalar; - return this; - }, - - /** - * Divides this point by a value and returns a new one - * TODO: rename in scalarDivide in 2.0 - * @param {Number} scalar - * @return {fabric.Point} - */ - divide: function (scalar) { - return new Point(this.x / scalar, this.y / scalar); - }, - - /** - * Divides this point by a value - * TODO: rename in scalarDivideEquals in 2.0 - * @param {Number} scalar - * @return {fabric.Point} thisArg - * @chainable - */ - divideEquals: function (scalar) { - this.x /= scalar; - this.y /= scalar; - return this; - }, - - /** - * Returns true if this point is equal to another one - * @param {fabric.Point} that - * @return {Boolean} - */ - eq: function (that) { - return (this.x === that.x && this.y === that.y); - }, - - /** - * Returns true if this point is less than another one - * @param {fabric.Point} that - * @return {Boolean} - */ - lt: function (that) { - return (this.x < that.x && this.y < that.y); - }, - - /** - * Returns true if this point is less than or equal to another one - * @param {fabric.Point} that - * @return {Boolean} - */ - lte: function (that) { - return (this.x <= that.x && this.y <= that.y); - }, - - /** - - * Returns true if this point is greater another one - * @param {fabric.Point} that - * @return {Boolean} - */ - gt: function (that) { - return (this.x > that.x && this.y > that.y); - }, - - /** - * Returns true if this point is greater than or equal to another one - * @param {fabric.Point} that - * @return {Boolean} - */ - gte: function (that) { - return (this.x >= that.x && this.y >= that.y); - }, - - /** - * Returns new point which is the result of linear interpolation with this one and another one - * @param {fabric.Point} that - * @param {Number} t , position of interpolation, between 0 and 1 default 0.5 - * @return {fabric.Point} - */ - lerp: function (that, t) { - if (typeof t === 'undefined') { - t = 0.5; - } - t = Math.max(Math.min(1, t), 0); - return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t); - }, - - /** - * Returns distance from this point and another one - * @param {fabric.Point} that - * @return {Number} - */ - distanceFrom: function (that) { - var dx = this.x - that.x, - dy = this.y - that.y; - return Math.sqrt(dx * dx + dy * dy); - }, - - /** - * Returns the point between this point and another one - * @param {fabric.Point} that - * @return {fabric.Point} - */ - midPointFrom: function (that) { - return this.lerp(that); - }, - - /** - * Returns a new point which is the min of this and another one - * @param {fabric.Point} that - * @return {fabric.Point} - */ - min: function (that) { - return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y)); - }, - - /** - * Returns a new point which is the max of this and another one - * @param {fabric.Point} that - * @return {fabric.Point} - */ - max: function (that) { - return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y)); - }, - - /** - * Returns string representation of this point - * @return {String} - */ - toString: function () { - return this.x + ',' + this.y; - }, - - /** - * Sets x/y of this point - * @param {Number} x - * @param {Number} y - * @chainable - */ - setXY: function (x, y) { - this.x = x; - this.y = y; - return this; - }, - - /** - * Sets x of this point - * @param {Number} x - * @chainable - */ - setX: function (x) { - this.x = x; - return this; - }, - - /** - * Sets y of this point - * @param {Number} y - * @chainable - */ - setY: function (y) { - this.y = y; - return this; - }, - - /** - * Sets x/y of this point from another point - * @param {fabric.Point} that - * @chainable - */ - setFromPoint: function (that) { - this.x = that.x; - this.y = that.y; - return this; - }, - - /** - * Swaps x/y of this point and another point - * @param {fabric.Point} that - */ - swap: function (that) { - var x = this.x, - y = this.y; - this.x = that.x; - this.y = that.y; - that.x = x; - that.y = y; - }, - - /** - * return a cloned instance of the point - * @return {fabric.Point} - */ - clone: function () { - return new Point(this.x, this.y); - } - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ - var fabric = global.fabric || (global.fabric = {}); - - if (fabric.Intersection) { - fabric.warn('fabric.Intersection is already defined'); - return; - } - - /** - * Intersection class - * @class fabric.Intersection - * @memberOf fabric - * @constructor - */ - function Intersection(status) { - this.status = status; - this.points = []; - } - - fabric.Intersection = Intersection; - - fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { - - constructor: Intersection, - - /** - * Appends a point to intersection - * @param {fabric.Point} point - * @return {fabric.Intersection} thisArg - * @chainable - */ - appendPoint: function (point) { - this.points.push(point); - return this; - }, - - /** - * Appends points to intersection - * @param {Array} points - * @return {fabric.Intersection} thisArg - * @chainable - */ - appendPoints: function (points) { - this.points = this.points.concat(points); - return this; - } - }; - - /** - * Checks if one line intersects another - * TODO: rename in intersectSegmentSegment - * @static - * @param {fabric.Point} a1 - * @param {fabric.Point} a2 - * @param {fabric.Point} b1 - * @param {fabric.Point} b2 - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { - var result, - uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), - ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), - uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); - if (uB !== 0) { - var ua = uaT / uB, - ub = ubT / uB; - if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { - result = new Intersection('Intersection'); - result.appendPoint(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); - } - else { - result = new Intersection(); - } - } - else { - if (uaT === 0 || ubT === 0) { - result = new Intersection('Coincident'); - } - else { - result = new Intersection('Parallel'); - } - } - return result; - }; - - /** - * Checks if line intersects polygon - * TODO: rename in intersectSegmentPolygon - * fix detection of coincident - * @static - * @param {fabric.Point} a1 - * @param {fabric.Point} a2 - * @param {Array} points - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectLinePolygon = function (a1, a2, points) { - var result = new Intersection(), - length = points.length, - b1, b2, inter, i; - - for (i = 0; i < length; i++) { - b1 = points[i]; - b2 = points[(i + 1) % length]; - inter = Intersection.intersectLineLine(a1, a2, b1, b2); - - result.appendPoints(inter.points); - } - if (result.points.length > 0) { - result.status = 'Intersection'; - } - return result; - }; - - /** - * Checks if polygon intersects another polygon - * @static - * @param {Array} points1 - * @param {Array} points2 - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { - var result = new Intersection(), - length = points1.length, i; - - for (i = 0; i < length; i++) { - var a1 = points1[i], - a2 = points1[(i + 1) % length], - inter = Intersection.intersectLinePolygon(a1, a2, points2); - - result.appendPoints(inter.points); - } - if (result.points.length > 0) { - result.status = 'Intersection'; - } - return result; - }; - - /** - * Checks if polygon intersects rectangle - * @static - * @param {Array} points - * @param {fabric.Point} r1 - * @param {fabric.Point} r2 - * @return {fabric.Intersection} - */ - fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { - var min = r1.min(r2), - max = r1.max(r2), - topRight = new fabric.Point(max.x, min.y), - bottomLeft = new fabric.Point(min.x, max.y), - inter1 = Intersection.intersectLinePolygon(min, topRight, points), - inter2 = Intersection.intersectLinePolygon(topRight, max, points), - inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), - inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), - result = new Intersection(); - - result.appendPoints(inter1.points); - result.appendPoints(inter2.points); - result.appendPoints(inter3.points); - result.appendPoints(inter4.points); - - if (result.points.length > 0) { - result.status = 'Intersection'; - } - return result; - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}); - - if (fabric.Color) { - fabric.warn('fabric.Color is already defined.'); - return; - } - - /** - * Color class - * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; - * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. - * - * @class fabric.Color - * @param {String} color optional in hex or rgb(a) or hsl format or from known color list - * @return {fabric.Color} thisArg - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#colors} - */ - function Color(color) { - if (!color) { - this.setSource([0, 0, 0, 1]); - } - else { - this._tryParsingColor(color); - } - } - - fabric.Color = Color; - - fabric.Color.prototype = /** @lends fabric.Color.prototype */ { - - /** - * @private - * @param {String|Array} color Color value to parse - */ - _tryParsingColor: function (color) { - var source; - - if (color in Color.colorNameMap) { - color = Color.colorNameMap[color]; - } - - if (color === 'transparent') { - source = [255, 255, 255, 0]; - } - - if (!source) { - source = Color.sourceFromHex(color); - } - if (!source) { - source = Color.sourceFromRgb(color); - } - if (!source) { - source = Color.sourceFromHsl(color); - } - if (!source) { - //if color is not recognize let's make black as canvas does - source = [0, 0, 0, 1]; - } - if (source) { - this.setSource(source); - } - }, - - /** - * Adapted from https://github.com/mjijackson - * @private - * @param {Number} r Red color value - * @param {Number} g Green color value - * @param {Number} b Blue color value - * @return {Array} Hsl color - */ - _rgbToHsl: function (r, g, b) { - r /= 255; g /= 255; b /= 255; - - var h, s, l, - max = fabric.util.array.max([r, g, b]), - min = fabric.util.array.min([r, g, b]); - - l = (max + min) / 2; - - if (max === min) { - h = s = 0; // achromatic - } - else { - var d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0); - break; - case g: - h = (b - r) / d + 2; - break; - case b: - h = (r - g) / d + 4; - break; - } - h /= 6; - } - - return [ - Math.round(h * 360), - Math.round(s * 100), - Math.round(l * 100) - ]; - }, - - /** - * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) - * @return {Array} - */ - getSource: function () { - return this._source; - }, - - /** - * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) - * @param {Array} source - */ - setSource: function (source) { - this._source = source; - }, - - /** - * Returns color representation in RGB format - * @return {String} ex: rgb(0-255,0-255,0-255) - */ - toRgb: function () { - var source = this.getSource(); - return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; - }, - - /** - * Returns color representation in RGBA format - * @return {String} ex: rgba(0-255,0-255,0-255,0-1) - */ - toRgba: function () { - var source = this.getSource(); - return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; - }, - - /** - * Returns color representation in HSL format - * @return {String} ex: hsl(0-360,0%-100%,0%-100%) - */ - toHsl: function () { - var source = this.getSource(), - hsl = this._rgbToHsl(source[0], source[1], source[2]); - - return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; - }, - - /** - * Returns color representation in HSLA format - * @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1) - */ - toHsla: function () { - var source = this.getSource(), - hsl = this._rgbToHsl(source[0], source[1], source[2]); - - return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; - }, - - /** - * Returns color representation in HEX format - * @return {String} ex: FF5555 - */ - toHex: function () { - var source = this.getSource(), r, g, b; - - r = source[0].toString(16); - r = (r.length === 1) ? ('0' + r) : r; - - g = source[1].toString(16); - g = (g.length === 1) ? ('0' + g) : g; - - b = source[2].toString(16); - b = (b.length === 1) ? ('0' + b) : b; - - return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); - }, - - /** - * Returns color representation in HEXA format - * @return {String} ex: FF5555CC - */ - toHexa: function () { - var source = this.getSource(), a; - - a = Math.round(source[3] * 255); - a = a.toString(16); - a = (a.length === 1) ? ('0' + a) : a; - - return this.toHex() + a.toUpperCase(); - }, - - /** - * Gets value of alpha channel for this color - * @return {Number} 0-1 - */ - getAlpha: function () { - return this.getSource()[3]; - }, - - /** - * Sets value of alpha channel for this color - * @param {Number} alpha Alpha value 0-1 - * @return {fabric.Color} thisArg - */ - setAlpha: function (alpha) { - var source = this.getSource(); - source[3] = alpha; - this.setSource(source); - return this; - }, - - /** - * Transforms color to its grayscale representation - * @return {fabric.Color} thisArg - */ - toGrayscale: function () { - var source = this.getSource(), - average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), - currentAlpha = source[3]; - this.setSource([average, average, average, currentAlpha]); - return this; - }, - - /** - * Transforms color to its black and white representation - * @param {Number} threshold - * @return {fabric.Color} thisArg - */ - toBlackWhite: function (threshold) { - var source = this.getSource(), - average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), - currentAlpha = source[3]; - - threshold = threshold || 127; - - average = (Number(average) < Number(threshold)) ? 0 : 255; - this.setSource([average, average, average, currentAlpha]); - return this; - }, - - /** - * Overlays color with another color - * @param {String|fabric.Color} otherColor - * @return {fabric.Color} thisArg - */ - overlayWith: function (otherColor) { - if (!(otherColor instanceof Color)) { - otherColor = new Color(otherColor); - } - - var result = [], - alpha = this.getAlpha(), - otherAlpha = 0.5, - source = this.getSource(), - otherSource = otherColor.getSource(), i; - - for (i = 0; i < 3; i++) { - result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); - } - - result[3] = alpha; - this.setSource(result); - return this; - } - }; - - /** - * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgba(255, 100, 10, 0.5), rgba( 255 , 100 , 10 , 0.5 ), rgb(1,1,1), rgba(100%, 60%, 10%, 0.5)) - * @static - * @field - * @memberOf fabric.Color - */ - // eslint-disable-next-line max-len - fabric.Color.reRGBa = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*((?:\d*\.?\d+)?)\s*)?\)$/i; - - /** - * Regex matching color in HSL or HSLA formats (ex: hsl(200, 80%, 10%), hsla(300, 50%, 80%, 0.5), hsla( 300 , 50% , 80% , 0.5 )) - * @static - * @field - * @memberOf fabric.Color - */ - fabric.Color.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/i; - - /** - * Regex matching color in HEX format (ex: #FF5544CC, #FF5555, 010155, aff) - * @static - * @field - * @memberOf fabric.Color - */ - fabric.Color.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i; - - /** - * Map of the 148 color names with HEX code - * @static - * @field - * @memberOf fabric.Color - * @see: https://www.w3.org/TR/css3-color/#svg-color - */ - fabric.Color.colorNameMap = { - aliceblue: '#F0F8FF', - antiquewhite: '#FAEBD7', - aqua: '#00FFFF', - aquamarine: '#7FFFD4', - azure: '#F0FFFF', - beige: '#F5F5DC', - bisque: '#FFE4C4', - black: '#000000', - blanchedalmond: '#FFEBCD', - blue: '#0000FF', - blueviolet: '#8A2BE2', - brown: '#A52A2A', - burlywood: '#DEB887', - cadetblue: '#5F9EA0', - chartreuse: '#7FFF00', - chocolate: '#D2691E', - coral: '#FF7F50', - cornflowerblue: '#6495ED', - cornsilk: '#FFF8DC', - crimson: '#DC143C', - cyan: '#00FFFF', - darkblue: '#00008B', - darkcyan: '#008B8B', - darkgoldenrod: '#B8860B', - darkgray: '#A9A9A9', - darkgrey: '#A9A9A9', - darkgreen: '#006400', - darkkhaki: '#BDB76B', - darkmagenta: '#8B008B', - darkolivegreen: '#556B2F', - darkorange: '#FF8C00', - darkorchid: '#9932CC', - darkred: '#8B0000', - darksalmon: '#E9967A', - darkseagreen: '#8FBC8F', - darkslateblue: '#483D8B', - darkslategray: '#2F4F4F', - darkslategrey: '#2F4F4F', - darkturquoise: '#00CED1', - darkviolet: '#9400D3', - deeppink: '#FF1493', - deepskyblue: '#00BFFF', - dimgray: '#696969', - dimgrey: '#696969', - dodgerblue: '#1E90FF', - firebrick: '#B22222', - floralwhite: '#FFFAF0', - forestgreen: '#228B22', - fuchsia: '#FF00FF', - gainsboro: '#DCDCDC', - ghostwhite: '#F8F8FF', - gold: '#FFD700', - goldenrod: '#DAA520', - gray: '#808080', - grey: '#808080', - green: '#008000', - greenyellow: '#ADFF2F', - honeydew: '#F0FFF0', - hotpink: '#FF69B4', - indianred: '#CD5C5C', - indigo: '#4B0082', - ivory: '#FFFFF0', - khaki: '#F0E68C', - lavender: '#E6E6FA', - lavenderblush: '#FFF0F5', - lawngreen: '#7CFC00', - lemonchiffon: '#FFFACD', - lightblue: '#ADD8E6', - lightcoral: '#F08080', - lightcyan: '#E0FFFF', - lightgoldenrodyellow: '#FAFAD2', - lightgray: '#D3D3D3', - lightgrey: '#D3D3D3', - lightgreen: '#90EE90', - lightpink: '#FFB6C1', - lightsalmon: '#FFA07A', - lightseagreen: '#20B2AA', - lightskyblue: '#87CEFA', - lightslategray: '#778899', - lightslategrey: '#778899', - lightsteelblue: '#B0C4DE', - lightyellow: '#FFFFE0', - lime: '#00FF00', - limegreen: '#32CD32', - linen: '#FAF0E6', - magenta: '#FF00FF', - maroon: '#800000', - mediumaquamarine: '#66CDAA', - mediumblue: '#0000CD', - mediumorchid: '#BA55D3', - mediumpurple: '#9370DB', - mediumseagreen: '#3CB371', - mediumslateblue: '#7B68EE', - mediumspringgreen: '#00FA9A', - mediumturquoise: '#48D1CC', - mediumvioletred: '#C71585', - midnightblue: '#191970', - mintcream: '#F5FFFA', - mistyrose: '#FFE4E1', - moccasin: '#FFE4B5', - navajowhite: '#FFDEAD', - navy: '#000080', - oldlace: '#FDF5E6', - olive: '#808000', - olivedrab: '#6B8E23', - orange: '#FFA500', - orangered: '#FF4500', - orchid: '#DA70D6', - palegoldenrod: '#EEE8AA', - palegreen: '#98FB98', - paleturquoise: '#AFEEEE', - palevioletred: '#DB7093', - papayawhip: '#FFEFD5', - peachpuff: '#FFDAB9', - peru: '#CD853F', - pink: '#FFC0CB', - plum: '#DDA0DD', - powderblue: '#B0E0E6', - purple: '#800080', - rebeccapurple: '#663399', - red: '#FF0000', - rosybrown: '#BC8F8F', - royalblue: '#4169E1', - saddlebrown: '#8B4513', - salmon: '#FA8072', - sandybrown: '#F4A460', - seagreen: '#2E8B57', - seashell: '#FFF5EE', - sienna: '#A0522D', - silver: '#C0C0C0', - skyblue: '#87CEEB', - slateblue: '#6A5ACD', - slategray: '#708090', - slategrey: '#708090', - snow: '#FFFAFA', - springgreen: '#00FF7F', - steelblue: '#4682B4', - tan: '#D2B48C', - teal: '#008080', - thistle: '#D8BFD8', - tomato: '#FF6347', - turquoise: '#40E0D0', - violet: '#EE82EE', - wheat: '#F5DEB3', - white: '#FFFFFF', - whitesmoke: '#F5F5F5', - yellow: '#FFFF00', - yellowgreen: '#9ACD32' - }; - - /** - * @private - * @param {Number} p - * @param {Number} q - * @param {Number} t - * @return {Number} - */ - function hue2rgb(p, q, t) { - if (t < 0) { - t += 1; - } - if (t > 1) { - t -= 1; - } - if (t < 1 / 6) { - return p + (q - p) * 6 * t; - } - if (t < 1 / 2) { - return q; - } - if (t < 2 / 3) { - return p + (q - p) * (2 / 3 - t) * 6; - } - return p; - } - - /** - * Returns new color object, when given a color in RGB format - * @memberOf fabric.Color - * @param {String} color Color value ex: rgb(0-255,0-255,0-255) - * @return {fabric.Color} - */ - fabric.Color.fromRgb = function (color) { - return Color.fromSource(Color.sourceFromRgb(color)); - }; - - /** - * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format - * @memberOf fabric.Color - * @param {String} color Color value ex: rgb(0-255,0-255,0-255), rgb(0%-100%,0%-100%,0%-100%) - * @return {Array} source - */ - fabric.Color.sourceFromRgb = function (color) { - var match = color.match(Color.reRGBa); - if (match) { - var r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), - g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), - b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); - - return [ - parseInt(r, 10), - parseInt(g, 10), - parseInt(b, 10), - match[4] ? parseFloat(match[4]) : 1 - ]; - } - }; - - /** - * Returns new color object, when given a color in RGBA format - * @static - * @function - * @memberOf fabric.Color - * @param {String} color - * @return {fabric.Color} - */ - fabric.Color.fromRgba = Color.fromRgb; - - /** - * Returns new color object, when given a color in HSL format - * @param {String} color Color value ex: hsl(0-260,0%-100%,0%-100%) - * @memberOf fabric.Color - * @return {fabric.Color} - */ - fabric.Color.fromHsl = function (color) { - return Color.fromSource(Color.sourceFromHsl(color)); - }; - - /** - * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HSL or HSLA format. - * Adapted from https://github.com/mjijackson - * @memberOf fabric.Color - * @param {String} color Color value ex: hsl(0-360,0%-100%,0%-100%) or hsla(0-360,0%-100%,0%-100%, 0-1) - * @return {Array} source - * @see http://http://www.w3.org/TR/css3-color/#hsl-color - */ - fabric.Color.sourceFromHsl = function (color) { - var match = color.match(Color.reHSLa); - if (!match) { - return; - } - - var h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, - s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), - l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), - r, g, b; - - if (s === 0) { - r = g = b = l; - } - else { - var q = l <= 0.5 ? l * (s + 1) : l + s - l * s, - p = l * 2 - q; - - r = hue2rgb(p, q, h + 1 / 3); - g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1 / 3); - } - - return [ - Math.round(r * 255), - Math.round(g * 255), - Math.round(b * 255), - match[4] ? parseFloat(match[4]) : 1 - ]; - }; - - /** - * Returns new color object, when given a color in HSLA format - * @static - * @function - * @memberOf fabric.Color - * @param {String} color - * @return {fabric.Color} - */ - fabric.Color.fromHsla = Color.fromHsl; - - /** - * Returns new color object, when given a color in HEX format - * @static - * @memberOf fabric.Color - * @param {String} color Color value ex: FF5555 - * @return {fabric.Color} - */ - fabric.Color.fromHex = function (color) { - return Color.fromSource(Color.sourceFromHex(color)); - }; - - /** - * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HEX format - * @static - * @memberOf fabric.Color - * @param {String} color ex: FF5555 or FF5544CC (RGBa) - * @return {Array} source - */ - fabric.Color.sourceFromHex = function (color) { - if (color.match(Color.reHex)) { - var value = color.slice(color.indexOf('#') + 1), - isShortNotation = (value.length === 3 || value.length === 4), - isRGBa = (value.length === 8 || value.length === 4), - r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), - g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), - b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6), - a = isRGBa ? (isShortNotation ? (value.charAt(3) + value.charAt(3)) : value.substring(6, 8)) : 'FF'; - - return [ - parseInt(r, 16), - parseInt(g, 16), - parseInt(b, 16), - parseFloat((parseInt(a, 16) / 255).toFixed(2)) - ]; - } - }; - - /** - * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) - * @static - * @memberOf fabric.Color - * @param {Array} source - * @return {fabric.Color} - */ - fabric.Color.fromSource = function (source) { - var oColor = new Color(); - oColor.setSource(source); - return oColor; - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - scaleMap = ['e', 'se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e'], - skewMap = ['ns', 'nesw', 'ew', 'nwse'], - controls = {}, - LEFT = 'left', TOP = 'top', RIGHT = 'right', BOTTOM = 'bottom', CENTER = 'center', - opposite = { - top: BOTTOM, - bottom: TOP, - left: RIGHT, - right: LEFT, - center: CENTER, - }, radiansToDegrees = fabric.util.radiansToDegrees, - sign = (Math.sign || function (x) { return ((x > 0) - (x < 0)) || +x; }); - - /** - * Combine control position and object angle to find the control direction compared - * to the object center. - * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls - * @param {fabric.Control} control the control class - * @return {Number} 0 - 7 a quadrant number - */ - function findCornerQuadrant(fabricObject, control) { - var cornerAngle = fabricObject.angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; - return Math.round((cornerAngle % 360) / 45); - } - - function fireEvent(eventName, options) { - var target = options.transform.target, - canvas = target.canvas, - canvasOptions = fabric.util.object.clone(options); - canvasOptions.target = target; - canvas && canvas.fire('object:' + eventName, canvasOptions); - target.fire(eventName, options); - } - - /** - * Inspect event and fabricObject properties to understand if the scaling action - * @param {Event} eventData from the user action - * @param {fabric.Object} fabricObject the fabric object about to scale - * @return {Boolean} true if scale is proportional - */ - function scaleIsProportional(eventData, fabricObject) { - var canvas = fabricObject.canvas, uniScaleKey = canvas.uniScaleKey, - uniformIsToggled = eventData[uniScaleKey]; - return (canvas.uniformScaling && !uniformIsToggled) || - (!canvas.uniformScaling && uniformIsToggled); - } - - /** - * Checks if transform is centered - * @param {Object} transform transform data - * @return {Boolean} true if transform is centered - */ - function isTransformCentered(transform) { - return transform.originX === CENTER && transform.originY === CENTER; - } - - /** - * Inspect fabricObject to understand if the current scaling action is allowed - * @param {fabric.Object} fabricObject the fabric object about to scale - * @param {String} by 'x' or 'y' or '' - * @param {Boolean} scaleProportionally true if we are trying to scale proportionally - * @return {Boolean} true if scaling is not allowed at current conditions - */ - function scalingIsForbidden(fabricObject, by, scaleProportionally) { - var lockX = fabricObject.lockScalingX, lockY = fabricObject.lockScalingY; - if (lockX && lockY) { - return true; - } - if (!by && (lockX || lockY) && scaleProportionally) { - return true; - } - if (lockX && by === 'x') { - return true; - } - if (lockY && by === 'y') { - return true; - } - return false; - } - - /** - * return the correct cursor style for the scale action - * @param {Event} eventData the javascript event that is causing the scale - * @param {fabric.Control} control the control that is interested in the action - * @param {fabric.Object} fabricObject the fabric object that is interested in the action - * @return {String} a valid css string for the cursor - */ - function scaleCursorStyleHandler(eventData, control, fabricObject) { - var notAllowed = 'not-allowed', - scaleProportionally = scaleIsProportional(eventData, fabricObject), - by = ''; - if (control.x !== 0 && control.y === 0) { - by = 'x'; - } - else if (control.x === 0 && control.y !== 0) { - by = 'y'; - } - if (scalingIsForbidden(fabricObject, by, scaleProportionally)) { - return notAllowed; - } - var n = findCornerQuadrant(fabricObject, control); - return scaleMap[n] + '-resize'; - } - - /** - * return the correct cursor style for the skew action - * @param {Event} eventData the javascript event that is causing the scale - * @param {fabric.Control} control the control that is interested in the action - * @param {fabric.Object} fabricObject the fabric object that is interested in the action - * @return {String} a valid css string for the cursor - */ - function skewCursorStyleHandler(eventData, control, fabricObject) { - var notAllowed = 'not-allowed'; - if (control.x !== 0 && fabricObject.lockSkewingY) { - return notAllowed; - } - if (control.y !== 0 && fabricObject.lockSkewingX) { - return notAllowed; - } - var n = findCornerQuadrant(fabricObject, control) % 4; - return skewMap[n] + '-resize'; - } - - /** - * Combine skew and scale style handlers to cover fabric standard use case - * @param {Event} eventData the javascript event that is causing the scale - * @param {fabric.Control} control the control that is interested in the action - * @param {fabric.Object} fabricObject the fabric object that is interested in the action - * @return {String} a valid css string for the cursor - */ - function scaleSkewCursorStyleHandler(eventData, control, fabricObject) { - if (eventData[fabricObject.canvas.altActionKey]) { - return controls.skewCursorStyleHandler(eventData, control, fabricObject); - } - return controls.scaleCursorStyleHandler(eventData, control, fabricObject); - } - - /** - * Inspect event, control and fabricObject to return the correct action name - * @param {Event} eventData the javascript event that is causing the scale - * @param {fabric.Control} control the control that is interested in the action - * @param {fabric.Object} fabricObject the fabric object that is interested in the action - * @return {String} an action name - */ - function scaleOrSkewActionName(eventData, control, fabricObject) { - var isAlternative = eventData[fabricObject.canvas.altActionKey]; - if (control.x === 0) { - // then is scaleY or skewX - return isAlternative ? 'skewX' : 'scaleY'; - } - if (control.y === 0) { - // then is scaleY or skewX - return isAlternative ? 'skewY' : 'scaleX'; - } - } - - /** - * Find the correct style for the control that is used for rotation. - * this function is very simple and it just take care of not-allowed or standard cursor - * @param {Event} eventData the javascript event that is causing the scale - * @param {fabric.Control} control the control that is interested in the action - * @param {fabric.Object} fabricObject the fabric object that is interested in the action - * @return {String} a valid css string for the cursor - */ - function rotationStyleHandler(eventData, control, fabricObject) { - if (fabricObject.lockRotation) { - return 'not-allowed'; - } - return control.cursorStyle; - } - - function commonEventInfo(eventData, transform, x, y) { - return { - e: eventData, - transform: transform, - pointer: { - x: x, - y: y, - } - }; - } - - /** - * Wrap an action handler with saving/restoring object position on the transform. - * this is the code that permits to objects to keep their position while transforming. - * @param {Function} actionHandler the function to wrap - * @return {Function} a function with an action handler signature - */ - function wrapWithFixedAnchor(actionHandler) { - return function (eventData, transform, x, y) { - var target = transform.target, centerPoint = target.getCenterPoint(), - constraint = target.translateToOriginPoint(centerPoint, transform.originX, transform.originY), - actionPerformed = actionHandler(eventData, transform, x, y); - target.setPositionByOrigin(constraint, transform.originX, transform.originY); - return actionPerformed; - }; - } - - /** - * Wrap an action handler with firing an event if the action is performed - * @param {Function} actionHandler the function to wrap - * @return {Function} a function with an action handler signature - */ - function wrapWithFireEvent(eventName, actionHandler) { - return function (eventData, transform, x, y) { - var actionPerformed = actionHandler(eventData, transform, x, y); - if (actionPerformed) { - fireEvent(eventName, commonEventInfo(eventData, transform, x, y)); - } - return actionPerformed; - }; - } - - /** - * Transforms a point described by x and y in a distance from the top left corner of the object - * bounding box. - * @param {Object} transform - * @param {String} originX - * @param {String} originY - * @param {number} x - * @param {number} y - * @return {Fabric.Point} the normalized point - */ - function getLocalPoint(transform, originX, originY, x, y) { - var target = transform.target, - control = target.controls[transform.corner], - zoom = target.canvas.getZoom(), - padding = target.padding / zoom, - localPoint = target.toLocalPoint(new fabric.Point(x, y), originX, originY); - if (localPoint.x >= padding) { - localPoint.x -= padding; - } - if (localPoint.x <= -padding) { - localPoint.x += padding; - } - if (localPoint.y >= padding) { - localPoint.y -= padding; - } - if (localPoint.y <= padding) { - localPoint.y += padding; - } - localPoint.x -= control.offsetX; - localPoint.y -= control.offsetY; - return localPoint; - } - - /** - * Detect if the fabric object is flipped on one side. - * @param {fabric.Object} target - * @return {Boolean} true if one flip, but not two. - */ - function targetHasOneFlip(target) { - return target.flipX !== target.flipY; - } - - /** - * Utility function to compensate the scale factor when skew is applied on both axes - * @private - */ - function compensateScaleForSkew(target, oppositeSkew, scaleToCompensate, axis, reference) { - if (target[oppositeSkew] !== 0) { - var newDim = target._getTransformedDimensions()[axis]; - var newValue = reference / newDim * target[scaleToCompensate]; - target.set(scaleToCompensate, newValue); - } - } - - /** - * Action handler for skewing on the X axis - * @private - */ - function skewObjectX(eventData, transform, x, y) { - var target = transform.target, - // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions(0, target.skewY), - localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), - // the mouse is in the center of the object, and we want it to stay there. - // so the object will grow twice as much as the mouse. - // this makes the skew growth to localPoint * 2 - dimNoSkew. - totalSkewSize = Math.abs(localPoint.x * 2) - dimNoSkew.x, - currentSkew = target.skewX, newSkew; - if (totalSkewSize < 2) { - // let's make it easy to go back to position 0. - newSkew = 0; - } - else { - newSkew = radiansToDegrees( - Math.atan2((totalSkewSize / target.scaleX), (dimNoSkew.y / target.scaleY)) - ); - // now we have to find the sign of the skew. - // it mostly depend on the origin of transformation. - if (transform.originX === LEFT && transform.originY === BOTTOM) { - newSkew = -newSkew; - } - if (transform.originX === RIGHT && transform.originY === TOP) { - newSkew = -newSkew; - } - if (targetHasOneFlip(target)) { - newSkew = -newSkew; - } - } - var hasSkewed = currentSkew !== newSkew; - if (hasSkewed) { - var dimBeforeSkewing = target._getTransformedDimensions().y; - target.set('skewX', newSkew); - compensateScaleForSkew(target, 'skewY', 'scaleY', 'y', dimBeforeSkewing); - } - return hasSkewed; - } - - /** - * Action handler for skewing on the Y axis - * @private - */ - function skewObjectY(eventData, transform, x, y) { - var target = transform.target, - // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions(target.skewX, 0), - localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), - // the mouse is in the center of the object, and we want it to stay there. - // so the object will grow twice as much as the mouse. - // this makes the skew growth to localPoint * 2 - dimNoSkew. - totalSkewSize = Math.abs(localPoint.y * 2) - dimNoSkew.y, - currentSkew = target.skewY, newSkew; - if (totalSkewSize < 2) { - // let's make it easy to go back to position 0. - newSkew = 0; - } - else { - newSkew = radiansToDegrees( - Math.atan2((totalSkewSize / target.scaleY), (dimNoSkew.x / target.scaleX)) - ); - // now we have to find the sign of the skew. - // it mostly depend on the origin of transformation. - if (transform.originX === LEFT && transform.originY === BOTTOM) { - newSkew = -newSkew; - } - if (transform.originX === RIGHT && transform.originY === TOP) { - newSkew = -newSkew; - } - if (targetHasOneFlip(target)) { - newSkew = -newSkew; - } - } - var hasSkewed = currentSkew !== newSkew; - if (hasSkewed) { - var dimBeforeSkewing = target._getTransformedDimensions().x; - target.set('skewY', newSkew); - compensateScaleForSkew(target, 'skewX', 'scaleX', 'x', dimBeforeSkewing); - } - return hasSkewed; - } - - /** - * Wrapped Action handler for skewing on the Y axis, takes care of the - * skew direction and determine the correct transform origin for the anchor point - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ - function skewHandlerX(eventData, transform, x, y) { - // step1 figure out and change transform origin. - // if skewX > 0 and originY bottom we anchor on right - // if skewX > 0 and originY top we anchor on left - // if skewX < 0 and originY bottom we anchor on left - // if skewX < 0 and originY top we anchor on right - // if skewX is 0, we look for mouse position to understand where are we going. - var target = transform.target, currentSkew = target.skewX, originX, originY = transform.originY; - if (target.lockSkewingX) { - return false; - } - if (currentSkew === 0) { - var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); - if (localPointFromCenter.x > 0) { - // we are pulling right, anchor left; - originX = LEFT; - } - else { - // we are pulling right, anchor right - originX = RIGHT; - } - } - else { - if (currentSkew > 0) { - originX = originY === TOP ? LEFT : RIGHT; - } - if (currentSkew < 0) { - originX = originY === TOP ? RIGHT : LEFT; - } - // is the object flipped on one side only? swap the origin. - if (targetHasOneFlip(target)) { - originX = originX === LEFT ? RIGHT : LEFT; - } - } - - // once we have the origin, we find the anchor point - transform.originX = originX; - var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectX)); - return finalHandler(eventData, transform, x, y); - } - - /** - * Wrapped Action handler for skewing on the Y axis, takes care of the - * skew direction and determine the correct transform origin for the anchor point - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ - function skewHandlerY(eventData, transform, x, y) { - // step1 figure out and change transform origin. - // if skewY > 0 and originX left we anchor on top - // if skewY > 0 and originX right we anchor on bottom - // if skewY < 0 and originX left we anchor on bottom - // if skewY < 0 and originX right we anchor on top - // if skewY is 0, we look for mouse position to understand where are we going. - var target = transform.target, currentSkew = target.skewY, originY, originX = transform.originX; - if (target.lockSkewingY) { - return false; - } - if (currentSkew === 0) { - var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); - if (localPointFromCenter.y > 0) { - // we are pulling down, anchor up; - originY = TOP; - } - else { - // we are pulling up, anchor down - originY = BOTTOM; - } - } - else { - if (currentSkew > 0) { - originY = originX === LEFT ? TOP : BOTTOM; - } - if (currentSkew < 0) { - originY = originX === LEFT ? BOTTOM : TOP; - } - // is the object flipped on one side only? swap the origin. - if (targetHasOneFlip(target)) { - originY = originY === TOP ? BOTTOM : TOP; - } - } - - // once we have the origin, we find the anchor point - transform.originY = originY; - var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectY)); - return finalHandler(eventData, transform, x, y); - } - - /** - * Action handler for rotation and snapping, without anchor point. - * Needs to be wrapped with `wrapWithFixedAnchor` to be effective - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - * @private - */ - function rotationWithSnapping(eventData, transform, x, y) { - var t = transform, - target = t.target, - pivotPoint = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); - - if (target.lockRotation) { - return false; - } - - var lastAngle = Math.atan2(t.ey - pivotPoint.y, t.ex - pivotPoint.x), - curAngle = Math.atan2(y - pivotPoint.y, x - pivotPoint.x), - angle = radiansToDegrees(curAngle - lastAngle + t.theta), - hasRotated = true; - - if (target.snapAngle > 0) { - var snapAngle = target.snapAngle, - snapThreshold = target.snapThreshold || snapAngle, - rightAngleLocked = Math.ceil(angle / snapAngle) * snapAngle, - leftAngleLocked = Math.floor(angle / snapAngle) * snapAngle; - - if (Math.abs(angle - leftAngleLocked) < snapThreshold) { - angle = leftAngleLocked; - } - else if (Math.abs(angle - rightAngleLocked) < snapThreshold) { - angle = rightAngleLocked; - } - } - - // normalize angle to positive value - if (angle < 0) { - angle = 360 + angle; - } - angle %= 360; - - hasRotated = target.angle !== angle; - target.angle = angle; - return hasRotated; - } - - /** - * Basic scaling logic, reused with different constrain for scaling X,Y, freely or equally. - * Needs to be wrapped with `wrapWithFixedAnchor` to be effective - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @param {Object} options additional information for scaling - * @param {String} options.by 'x', 'y', 'equally' or '' to indicate type of scaling - * @return {Boolean} true if some change happened - * @private - */ - function scaleObject(eventData, transform, x, y, options) { - options = options || {}; - var target = transform.target, - lockScalingX = target.lockScalingX, lockScalingY = target.lockScalingY, - by = options.by, newPoint, scaleX, scaleY, dim, - scaleProportionally = scaleIsProportional(eventData, target), - forbidScaling = scalingIsForbidden(target, by, scaleProportionally), - signX, signY, gestureScale = transform.gestureScale; - - if (forbidScaling) { - return false; - } - if (gestureScale) { - scaleX = transform.scaleX * gestureScale; - scaleY = transform.scaleY * gestureScale; - } - else { - newPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y); - // use of sign: We use sign to detect change of direction of an action. sign usually change when - // we cross the origin point with the mouse. So a scale flip for example. There is an issue when scaling - // by center and scaling using one middle control ( default: mr, mt, ml, mb), the mouse movement can easily - // cross many time the origin point and flip the object. so we need a way to filter out the noise. - // This ternary here should be ok to filter out X scaling when we want Y only and vice versa. - signX = by !== 'y' ? sign(newPoint.x) : 1; - signY = by !== 'x' ? sign(newPoint.y) : 1; - if (!transform.signX) { - transform.signX = signX; - } - if (!transform.signY) { - transform.signY = signY; - } - - if (target.lockScalingFlip && - (transform.signX !== signX || transform.signY !== signY) - ) { - return false; - } - - dim = target._getTransformedDimensions(); - // missing detection of flip and logic to switch the origin - if (scaleProportionally && !by) { - // uniform scaling - var distance = Math.abs(newPoint.x) + Math.abs(newPoint.y), - original = transform.original, - originalDistance = Math.abs(dim.x * original.scaleX / target.scaleX) + - Math.abs(dim.y * original.scaleY / target.scaleY), - scale = distance / originalDistance; - scaleX = original.scaleX * scale; - scaleY = original.scaleY * scale; - } - else { - scaleX = Math.abs(newPoint.x * target.scaleX / dim.x); - scaleY = Math.abs(newPoint.y * target.scaleY / dim.y); - } - // if we are scaling by center, we need to double the scale - if (isTransformCentered(transform)) { - scaleX *= 2; - scaleY *= 2; - } - if (transform.signX !== signX && by !== 'y') { - transform.originX = opposite[transform.originX]; - scaleX *= -1; - transform.signX = signX; - } - if (transform.signY !== signY && by !== 'x') { - transform.originY = opposite[transform.originY]; - scaleY *= -1; - transform.signY = signY; - } - } - // minScale is taken are in the setter. - var oldScaleX = target.scaleX, oldScaleY = target.scaleY; - if (!by) { - !lockScalingX && target.set('scaleX', scaleX); - !lockScalingY && target.set('scaleY', scaleY); - } - else { - // forbidden cases already handled on top here. - by === 'x' && target.set('scaleX', scaleX); - by === 'y' && target.set('scaleY', scaleY); - } - return oldScaleX !== target.scaleX || oldScaleY !== target.scaleY; - } - - /** - * Generic scaling logic, to scale from corners either equally or freely. - * Needs to be wrapped with `wrapWithFixedAnchor` to be effective - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ - function scaleObjectFromCorner(eventData, transform, x, y) { - return scaleObject(eventData, transform, x, y); - } - - /** - * Scaling logic for the X axis. - * Needs to be wrapped with `wrapWithFixedAnchor` to be effective - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ - function scaleObjectX(eventData, transform, x, y) { - return scaleObject(eventData, transform, x, y, { by: 'x' }); - } - - /** - * Scaling logic for the Y axis. - * Needs to be wrapped with `wrapWithFixedAnchor` to be effective - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ - function scaleObjectY(eventData, transform, x, y) { - return scaleObject(eventData, transform, x, y, { by: 'y' }); - } - - /** - * Composed action handler to either scale Y or skew X - * Needs to be wrapped with `wrapWithFixedAnchor` to be effective - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ - function scalingYOrSkewingX(eventData, transform, x, y) { - // ok some safety needed here. - if (eventData[transform.target.canvas.altActionKey]) { - return controls.skewHandlerX(eventData, transform, x, y); - } - return controls.scalingY(eventData, transform, x, y); - } - - /** - * Composed action handler to either scale X or skew Y - * Needs to be wrapped with `wrapWithFixedAnchor` to be effective - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ - function scalingXOrSkewingY(eventData, transform, x, y) { - // ok some safety needed here. - if (eventData[transform.target.canvas.altActionKey]) { - return controls.skewHandlerY(eventData, transform, x, y); - } - return controls.scalingX(eventData, transform, x, y); - } - - /** - * Action handler to change textbox width - * Needs to be wrapped with `wrapWithFixedAnchor` to be effective - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ - function changeWidth(eventData, transform, x, y) { - var target = transform.target, localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), - strokePadding = target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), - multiplier = isTransformCentered(transform) ? 2 : 1, - oldWidth = target.width, - newWidth = Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding; - target.set('width', Math.max(newWidth, 0)); - return oldWidth !== newWidth; - } - - /** - * Action handler - * @private - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if the translation occurred - */ - function dragHandler(eventData, transform, x, y) { - var target = transform.target, - newLeft = x - transform.offsetX, - newTop = y - transform.offsetY, - moveX = !target.get('lockMovementX') && target.left !== newLeft, - moveY = !target.get('lockMovementY') && target.top !== newTop; - moveX && target.set('left', newLeft); - moveY && target.set('top', newTop); - if (moveX || moveY) { - fireEvent('moving', commonEventInfo(eventData, transform, x, y)); - } - return moveX || moveY; - } - - controls.scaleCursorStyleHandler = scaleCursorStyleHandler; - controls.skewCursorStyleHandler = skewCursorStyleHandler; - controls.scaleSkewCursorStyleHandler = scaleSkewCursorStyleHandler; - controls.rotationWithSnapping = wrapWithFireEvent('rotating', wrapWithFixedAnchor(rotationWithSnapping)); - controls.scalingEqually = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectFromCorner)); - controls.scalingX = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectX)); - controls.scalingY = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectY)); - controls.scalingYOrSkewingX = scalingYOrSkewingX; - controls.scalingXOrSkewingY = scalingXOrSkewingY; - controls.changeWidth = wrapWithFireEvent('resizing', wrapWithFixedAnchor(changeWidth)); - controls.skewHandlerX = skewHandlerX; - controls.skewHandlerY = skewHandlerY; - controls.dragHandler = dragHandler; - controls.scaleOrSkewActionName = scaleOrSkewActionName; - controls.rotationStyleHandler = rotationStyleHandler; - controls.fireEvent = fireEvent; - controls.wrapWithFixedAnchor = wrapWithFixedAnchor; - controls.wrapWithFireEvent = wrapWithFireEvent; - controls.getLocalPoint = getLocalPoint; - fabric.controlsUtils = controls; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - degreesToRadians = fabric.util.degreesToRadians, - controls = fabric.controlsUtils; - - /** - * Render a round control, as per fabric features. - * This function is written to respect object properties like transparentCorners, cornerSize - * cornerColor, cornerStrokeColor - * plus the addition of offsetY and offsetX. - * @param {CanvasRenderingContext2D} ctx context to render on - * @param {Number} left x coordinate where the control center should be - * @param {Number} top y coordinate where the control center should be - * @param {Object} styleOverride override for fabric.Object controls style - * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls - */ - function renderCircleControl(ctx, left, top, styleOverride, fabricObject) { - styleOverride = styleOverride || {}; - var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, - ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, - transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? - styleOverride.transparentCorners : fabricObject.transparentCorners, - methodName = transparentCorners ? 'stroke' : 'fill', - stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor), - myLeft = left, - myTop = top, size; - ctx.save(); - ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; - ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; - // as soon as fabric react v5, remove ie11, use proper ellipse code. - if (xSize > ySize) { - size = xSize; - ctx.scale(1.0, ySize / xSize); - myTop = top * xSize / ySize; - } - else if (ySize > xSize) { - size = ySize; - ctx.scale(xSize / ySize, 1.0); - myLeft = left * ySize / xSize; - } - else { - size = xSize; - } - // this is still wrong - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.arc(myLeft, myTop, size / 2, 0, 2 * Math.PI, false); - ctx[methodName](); - if (stroke) { - ctx.stroke(); - } - ctx.restore(); - } - - /** - * Render a square control, as per fabric features. - * This function is written to respect object properties like transparentCorners, cornerSize - * cornerColor, cornerStrokeColor - * plus the addition of offsetY and offsetX. - * @param {CanvasRenderingContext2D} ctx context to render on - * @param {Number} left x coordinate where the control center should be - * @param {Number} top y coordinate where the control center should be - * @param {Object} styleOverride override for fabric.Object controls style - * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls - */ - function renderSquareControl(ctx, left, top, styleOverride, fabricObject) { - styleOverride = styleOverride || {}; - var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, - ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, - transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? - styleOverride.transparentCorners : fabricObject.transparentCorners, - methodName = transparentCorners ? 'stroke' : 'fill', - stroke = !transparentCorners && ( - styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor - ), xSizeBy2 = xSize / 2, ySizeBy2 = ySize / 2; - ctx.save(); - ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; - ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; - // this is still wrong - ctx.lineWidth = 1; - ctx.translate(left, top); - ctx.rotate(degreesToRadians(fabricObject.angle)); - // this does not work, and fixed with ( && ) does not make sense. - // to have real transparent corners we need the controls on upperCanvas - // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); - ctx[methodName + 'Rect'](-xSizeBy2, -ySizeBy2, xSize, ySize); - if (stroke) { - ctx.strokeRect(-xSizeBy2, -ySizeBy2, xSize, ySize); - } - ctx.restore(); - } - - controls.renderCircleControl = renderCircleControl; - controls.renderSquareControl = renderSquareControl; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}); - - function Control(options) { - for (var i in options) { - this[i] = options[i]; - } - } - - fabric.Control = Control; - - fabric.Control.prototype = /** @lends fabric.Control.prototype */ { - - /** - * keep track of control visibility. - * mainly for backward compatibility. - * if you do not want to see a control, you can remove it - * from the controlset. - * @type {Boolean} - * @default true - */ - visible: true, - - /** - * Name of the action that the control will likely execute. - * This is optional. FabricJS uses to identify what the user is doing for some - * extra optimizations. If you are writing a custom control and you want to know - * somewhere else in the code what is going on, you can use this string here. - * you can also provide a custom getActionName if your control run multiple actions - * depending on some external state. - * default to scale since is the most common, used on 4 corners by default - * @type {String} - * @default 'scale' - */ - actionName: 'scale', - - /** - * Drawing angle of the control. - * NOT used for now, but name marked as needed for internal logic - * example: to reuse the same drawing function for different rotated controls - * @type {Number} - * @default 0 - */ - angle: 0, - - /** - * Relative position of the control. X - * 0,0 is the center of the Object, while -0.5 (left) or 0.5 (right) are the extremities - * of the bounding box. - * @type {Number} - * @default 0 - */ - x: 0, - - /** - * Relative position of the control. Y - * 0,0 is the center of the Object, while -0.5 (top) or 0.5 (bottom) are the extremities - * of the bounding box. - * @type {Number} - * @default 0 - */ - y: 0, - - /** - * Horizontal offset of the control from the defined position. In pixels - * Positive offset moves the control to the right, negative to the left. - * It used when you want to have position of control that does not scale with - * the bounding box. Example: rotation control is placed at x:0, y: 0.5 on - * the boundindbox, with an offset of 30 pixels vertically. Those 30 pixels will - * stay 30 pixels no matter how the object is big. Another example is having 2 - * controls in the corner, that stay in the same position when the object scale. - * of the bounding box. - * @type {Number} - * @default 0 - */ - offsetX: 0, - - /** - * Vertical offset of the control from the defined position. In pixels - * Positive offset moves the control to the bottom, negative to the top. - * @type {Number} - * @default 0 - */ - offsetY: 0, - - /** - * Sets the length of the control. If null, defaults to object's cornerSize. - * Expects both sizeX and sizeY to be set when set. - * @type {?Number} - * @default null - */ - sizeX: null, - - /** - * Sets the height of the control. If null, defaults to object's cornerSize. - * Expects both sizeX and sizeY to be set when set. - * @type {?Number} - * @default null - */ - sizeY: null, - - /** - * Sets the length of the touch area of the control. If null, defaults to object's touchCornerSize. - * Expects both touchSizeX and touchSizeY to be set when set. - * @type {?Number} - * @default null - */ - touchSizeX: null, - - /** - * Sets the height of the touch area of the control. If null, defaults to object's touchCornerSize. - * Expects both touchSizeX and touchSizeY to be set when set. - * @type {?Number} - * @default null - */ - touchSizeY: null, - - /** - * Css cursor style to display when the control is hovered. - * if the method `cursorStyleHandler` is provided, this property is ignored. - * @type {String} - * @default 'crosshair' - */ - cursorStyle: 'crosshair', - - /** - * If controls has an offsetY or offsetX, draw a line that connects - * the control to the bounding box - * @type {Boolean} - * @default false - */ - withConnection: false, - - /** - * The control actionHandler, provide one to handle action ( control being moved ) - * @param {Event} eventData the native mouse event - * @param {Object} transformData properties of the current transform - * @param {Number} x x position of the cursor - * @param {Number} y y position of the cursor - * @return {Boolean} true if the action/event modified the object - */ - actionHandler: function (/* eventData, transformData, x, y */) { }, - - /** - * The control handler for mouse down, provide one to handle mouse down on control - * @param {Event} eventData the native mouse event - * @param {Object} transformData properties of the current transform - * @param {Number} x x position of the cursor - * @param {Number} y y position of the cursor - * @return {Boolean} true if the action/event modified the object - */ - mouseDownHandler: function (/* eventData, transformData, x, y */) { }, - - /** - * The control mouseUpHandler, provide one to handle an effect on mouse up. - * @param {Event} eventData the native mouse event - * @param {Object} transformData properties of the current transform - * @param {Number} x x position of the cursor - * @param {Number} y y position of the cursor - * @return {Boolean} true if the action/event modified the object - */ - mouseUpHandler: function (/* eventData, transformData, x, y */) { }, - - /** - * Returns control actionHandler - * @param {Event} eventData the native mouse event - * @param {fabric.Object} fabricObject on which the control is displayed - * @param {fabric.Control} control control for which the action handler is being asked - * @return {Function} the action handler - */ - getActionHandler: function (/* eventData, fabricObject, control */) { - return this.actionHandler; - }, - - /** - * Returns control mouseDown handler - * @param {Event} eventData the native mouse event - * @param {fabric.Object} fabricObject on which the control is displayed - * @param {fabric.Control} control control for which the action handler is being asked - * @return {Function} the action handler - */ - getMouseDownHandler: function (/* eventData, fabricObject, control */) { - return this.mouseDownHandler; - }, - - /** - * Returns control mouseUp handler - * @param {Event} eventData the native mouse event - * @param {fabric.Object} fabricObject on which the control is displayed - * @param {fabric.Control} control control for which the action handler is being asked - * @return {Function} the action handler - */ - getMouseUpHandler: function (/* eventData, fabricObject, control */) { - return this.mouseUpHandler; - }, - - /** - * Returns control cursorStyle for css using cursorStyle. If you need a more elaborate - * function you can pass one in the constructor - * the cursorStyle property - * @param {Event} eventData the native mouse event - * @param {fabric.Control} control the current control ( likely this) - * @param {fabric.Object} object on which the control is displayed - * @return {String} - */ - cursorStyleHandler: function (eventData, control /* fabricObject */) { - return control.cursorStyle; - }, - - /** - * Returns the action name. The basic implementation just return the actionName property. - * @param {Event} eventData the native mouse event - * @param {fabric.Control} control the current control ( likely this) - * @param {fabric.Object} object on which the control is displayed - * @return {String} - */ - getActionName: function (eventData, control /* fabricObject */) { - return control.actionName; - }, - - /** - * Returns controls visibility - * @param {fabric.Object} object on which the control is displayed - * @param {String} controlKey key where the control is memorized on the - * @return {Boolean} - */ - getVisibility: function (fabricObject, controlKey) { - var objectVisibility = fabricObject._controlsVisibility; - if (objectVisibility && typeof objectVisibility[controlKey] !== 'undefined') { - return objectVisibility[controlKey]; - } - return this.visible; - }, - - /** - * Sets controls visibility - * @param {Boolean} visibility for the object - * @return {Void} - */ - setVisibility: function (visibility /* name, fabricObject */) { - this.visible = visibility; - }, - - - positionHandler: function (dim, finalMatrix /*, fabricObject, currentControl */) { - var point = fabric.util.transformPoint({ - x: this.x * dim.x + this.offsetX, - y: this.y * dim.y + this.offsetY - }, finalMatrix); - return point; - }, - - /** - * Returns the coords for this control based on object values. - * @param {Number} objectAngle angle from the fabric object holding the control - * @param {Number} objectCornerSize cornerSize from the fabric object holding the control (or touchCornerSize if - * isTouch is true) - * @param {Number} centerX x coordinate where the control center should be - * @param {Number} centerY y coordinate where the control center should be - * @param {boolean} isTouch true if touch corner, false if normal corner - */ - calcCornerCoords: function (objectAngle, objectCornerSize, centerX, centerY, isTouch) { - var cosHalfOffset, - sinHalfOffset, - cosHalfOffsetComp, - sinHalfOffsetComp, - xSize = (isTouch) ? this.touchSizeX : this.sizeX, - ySize = (isTouch) ? this.touchSizeY : this.sizeY; - if (xSize && ySize && xSize !== ySize) { - // handle rectangular corners - var controlTriangleAngle = Math.atan2(ySize, xSize); - var cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2; - var newTheta = controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); - var newThetaComp = Math.PI / 2 - controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); - cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta); - sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta); - // use complementary angle for two corners - cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newThetaComp); - sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newThetaComp); - } - else { - // handle square corners - // use default object corner size unless size is defined - var cornerSize = (xSize && ySize) ? xSize : objectCornerSize; - /* 0.7071067812 stands for sqrt(2)/2 */ - cornerHypotenuse = cornerSize * 0.7071067812; - // complementary angles are equal since they're both 45 degrees - var newTheta = fabric.util.degreesToRadians(45 - objectAngle); - cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newTheta); - sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newTheta); - } - - return { - tl: { - x: centerX - sinHalfOffsetComp, - y: centerY - cosHalfOffsetComp, - }, - tr: { - x: centerX + cosHalfOffset, - y: centerY - sinHalfOffset, - }, - bl: { - x: centerX - cosHalfOffset, - y: centerY + sinHalfOffset, - }, - br: { - x: centerX + sinHalfOffsetComp, - y: centerY + cosHalfOffsetComp, - }, - }; - }, - - /** - * Render function for the control. - * When this function runs the context is unscaled. unrotate. Just retina scaled. - * all the functions will have to translate to the point left,top before starting Drawing - * if they want to draw a control where the position is detected. - * left and top are the result of the positionHandler function - * @param {RenderingContext2D} ctx the context where the control will be drawn - * @param {Number} left position of the canvas where we are about to render the control. - * @param {Number} top position of the canvas where we are about to render the control. - * @param {Object} styleOverride - * @param {fabric.Object} fabricObject the object where the control is about to be rendered - */ - render: function (ctx, left, top, styleOverride, fabricObject) { - styleOverride = styleOverride || {}; - switch (styleOverride.cornerStyle || fabricObject.cornerStyle) { - case 'circle': - fabric.controlsUtils.renderCircleControl.call(this, ctx, left, top, styleOverride, fabricObject); - break; - default: - fabric.controlsUtils.renderSquareControl.call(this, ctx, left, top, styleOverride, fabricObject); - } - }, - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function () { - - /* _FROM_SVG_START_ */ - function getColorStop(el, multiplier) { - var style = el.getAttribute('style'), - offset = el.getAttribute('offset') || 0, - color, colorAlpha, opacity, i; - - // convert percents to absolute values - offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); - offset = offset < 0 ? 0 : offset > 1 ? 1 : offset; - if (style) { - var keyValuePairs = style.split(/\s*;\s*/); - - if (keyValuePairs[keyValuePairs.length - 1] === '') { - keyValuePairs.pop(); - } - - for (i = keyValuePairs.length; i--;) { - - var split = keyValuePairs[i].split(/\s*:\s*/), - key = split[0].trim(), - value = split[1].trim(); - - if (key === 'stop-color') { - color = value; - } - else if (key === 'stop-opacity') { - opacity = value; - } - } - } - - if (!color) { - color = el.getAttribute('stop-color') || 'rgb(0,0,0)'; - } - if (!opacity) { - opacity = el.getAttribute('stop-opacity'); - } - - color = new fabric.Color(color); - colorAlpha = color.getAlpha(); - opacity = isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity); - opacity *= colorAlpha * multiplier; - - return { - offset: offset, - color: color.toRgb(), - opacity: opacity - }; - } - - function getLinearCoords(el) { - return { - x1: el.getAttribute('x1') || 0, - y1: el.getAttribute('y1') || 0, - x2: el.getAttribute('x2') || '100%', - y2: el.getAttribute('y2') || 0 - }; - } - - function getRadialCoords(el) { - return { - x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', - y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', - r1: 0, - x2: el.getAttribute('cx') || '50%', - y2: el.getAttribute('cy') || '50%', - r2: el.getAttribute('r') || '50%' - }; - } - /* _FROM_SVG_END_ */ - - var clone = fabric.util.object.clone; - - /** - * Gradient class - * @class fabric.Gradient - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#gradients} - * @see {@link fabric.Gradient#initialize} for constructor definition - */ - fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { - - /** - * Horizontal offset for aligning gradients coming from SVG when outside pathgroups - * @type Number - * @default 0 - */ - offsetX: 0, - - /** - * Vertical offset for aligning gradients coming from SVG when outside pathgroups - * @type Number - * @default 0 - */ - offsetY: 0, - - /** - * A transform matrix to apply to the gradient before painting. - * Imported from svg gradients, is not applied with the current transform in the center. - * Before this transform is applied, the origin point is at the top left corner of the object - * plus the addition of offsetY and offsetX. - * @type Number[] - * @default null - */ - gradientTransform: null, - - /** - * coordinates units for coords. - * If `pixels`, the number of coords are in the same unit of width / height. - * If set as `percentage` the coords are still a number, but 1 means 100% of width - * for the X and 100% of the height for the y. It can be bigger than 1 and negative. - * allowed values pixels or percentage. - * @type String - * @default 'pixels' - */ - gradientUnits: 'pixels', - - /** - * Gradient type linear or radial - * @type String - * @default 'pixels' - */ - type: 'linear', - - /** - * Constructor - * @param {Object} options Options object with type, coords, gradientUnits and colorStops - * @param {Object} [options.type] gradient type linear or radial - * @param {Object} [options.gradientUnits] gradient units - * @param {Object} [options.offsetX] SVG import compatibility - * @param {Object} [options.offsetY] SVG import compatibility - * @param {Object[]} options.colorStops contains the colorstops. - * @param {Object} options.coords contains the coords of the gradient - * @param {Number} [options.coords.x1] X coordiante of the first point for linear or of the focal point for radial - * @param {Number} [options.coords.y1] Y coordiante of the first point for linear or of the focal point for radial - * @param {Number} [options.coords.x2] X coordiante of the second point for linear or of the center point for radial - * @param {Number} [options.coords.y2] Y coordiante of the second point for linear or of the center point for radial - * @param {Number} [options.coords.r1] only for radial gradient, radius of the inner circle - * @param {Number} [options.coords.r2] only for radial gradient, radius of the external circle - * @return {fabric.Gradient} thisArg - */ - initialize: function (options) { - options || (options = {}); - options.coords || (options.coords = {}); - - var coords, _this = this; - - // sets everything, then coords and colorstops get sets again - Object.keys(options).forEach(function (option) { - _this[option] = options[option]; - }); - - if (this.id) { - this.id += '_' + fabric.Object.__uid++; - } - else { - this.id = fabric.Object.__uid++; - } - - coords = { - x1: options.coords.x1 || 0, - y1: options.coords.y1 || 0, - x2: options.coords.x2 || 0, - y2: options.coords.y2 || 0 - }; - - if (this.type === 'radial') { - coords.r1 = options.coords.r1 || 0; - coords.r2 = options.coords.r2 || 0; - } - - this.coords = coords; - this.colorStops = options.colorStops.slice(); - }, - - /** - * Adds another colorStop - * @param {Object} colorStop Object with offset and color - * @return {fabric.Gradient} thisArg - */ - addColorStop: function (colorStops) { - for (var position in colorStops) { - var color = new fabric.Color(colorStops[position]); - this.colorStops.push({ - offset: parseFloat(position), - color: color.toRgb(), - opacity: color.getAlpha() - }); - } - return this; - }, - - /** - * Returns object representation of a gradient - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} - */ - toObject: function (propertiesToInclude) { - var object = { - type: this.type, - coords: this.coords, - colorStops: this.colorStops, - offsetX: this.offsetX, - offsetY: this.offsetY, - gradientUnits: this.gradientUnits, - gradientTransform: this.gradientTransform ? this.gradientTransform.concat() : this.gradientTransform - }; - fabric.util.populateWithProperties(this, object, propertiesToInclude); - - return object; - }, - - /* _TO_SVG_START_ */ - /** - * Returns SVG representation of an gradient - * @param {Object} object Object to create a gradient for - * @return {String} SVG representation of an gradient (linear/radial) - */ - toSVG: function (object, options) { - var coords = clone(this.coords, true), i, len, options = options || {}, - markup, commonAttributes, colorStops = clone(this.colorStops, true), - needsSwap = coords.r1 > coords.r2, - transform = this.gradientTransform ? this.gradientTransform.concat() : fabric.iMatrix.concat(), - offsetX = -this.offsetX, offsetY = -this.offsetY, - withViewport = !!options.additionalTransform, - gradientUnits = this.gradientUnits === 'pixels' ? 'userSpaceOnUse' : 'objectBoundingBox'; - // colorStops must be sorted ascending - colorStops.sort(function (a, b) { - return a.offset - b.offset; - }); - - if (gradientUnits === 'objectBoundingBox') { - offsetX /= object.width; - offsetY /= object.height; - } - else { - offsetX += object.width / 2; - offsetY += object.height / 2; - } - if (object.type === 'path' && this.gradientUnits !== 'percentage') { - offsetX -= object.pathOffset.x; - offsetY -= object.pathOffset.y; - } - - - transform[4] -= offsetX; - transform[5] -= offsetY; - - commonAttributes = 'id="SVGID_' + this.id + - '" gradientUnits="' + gradientUnits + '"'; - commonAttributes += ' gradientTransform="' + (withViewport ? - options.additionalTransform + ' ' : '') + fabric.util.matrixToSVG(transform) + '" '; - - if (this.type === 'linear') { - markup = [ - '\n' - ]; - } - else if (this.type === 'radial') { - // svg radial gradient has just 1 radius. the biggest. - markup = [ - '\n' - ]; - } - - if (this.type === 'radial') { - if (needsSwap) { - // svg goes from internal to external radius. if radius are inverted, swap color stops. - colorStops = colorStops.concat(); - colorStops.reverse(); - for (i = 0, len = colorStops.length; i < len; i++) { - colorStops[i].offset = 1 - colorStops[i].offset; - } - } - var minRadius = Math.min(coords.r1, coords.r2); - if (minRadius > 0) { - // i have to shift all colorStops and add new one in 0. - var maxRadius = Math.max(coords.r1, coords.r2), - percentageShift = minRadius / maxRadius; - for (i = 0, len = colorStops.length; i < len; i++) { - colorStops[i].offset += percentageShift * (1 - colorStops[i].offset); - } - } - } - - for (i = 0, len = colorStops.length; i < len; i++) { - var colorStop = colorStops[i]; - markup.push( - '\n' - ); - } - - markup.push((this.type === 'linear' ? '\n' : '\n')); - - return markup.join(''); - }, - /* _TO_SVG_END_ */ - - /** - * Returns an instance of CanvasGradient - * @param {CanvasRenderingContext2D} ctx Context to render on - * @return {CanvasGradient} - */ - toLive: function (ctx) { - var gradient, coords = fabric.util.object.clone(this.coords), i, len; - - if (!this.type) { - return; - } - - if (this.type === 'linear') { - gradient = ctx.createLinearGradient( - coords.x1, coords.y1, coords.x2, coords.y2); - } - else if (this.type === 'radial') { - gradient = ctx.createRadialGradient( - coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); - } - - for (i = 0, len = this.colorStops.length; i < len; i++) { - var color = this.colorStops[i].color, - opacity = this.colorStops[i].opacity, - offset = this.colorStops[i].offset; - - if (typeof opacity !== 'undefined') { - color = new fabric.Color(color).setAlpha(opacity).toRgba(); - } - gradient.addColorStop(offset, color); - } - - return gradient; - } - }); - - fabric.util.object.extend(fabric.Gradient, { - - /* _FROM_SVG_START_ */ - /** - * Returns {@link fabric.Gradient} instance from an SVG element - * @static - * @memberOf fabric.Gradient - * @param {SVGGradientElement} el SVG gradient element - * @param {fabric.Object} instance - * @param {String} opacityAttr A fill-opacity or stroke-opacity attribute to multiply to each stop's opacity. - * @param {Object} svgOptions an object containing the size of the SVG in order to parse correctly gradients - * that uses gradientUnits as 'userSpaceOnUse' and percentages. - * @param {Object.number} viewBoxWidth width part of the viewBox attribute on svg - * @param {Object.number} viewBoxHeight height part of the viewBox attribute on svg - * @param {Object.number} width width part of the svg tag if viewBox is not specified - * @param {Object.number} height height part of the svg tag if viewBox is not specified - * @return {fabric.Gradient} Gradient instance - * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement - * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement - */ - fromElement: function (el, instance, opacityAttr, svgOptions) { - /** - * @example: - * - * - * - * - * - * - * OR - * - * - * - * - * - * - * OR - * - * - * - * - * - * - * - * OR - * - * - * - * - * - * - * - */ - - var multiplier = parseFloat(opacityAttr) / (/%$/.test(opacityAttr) ? 100 : 1); - multiplier = multiplier < 0 ? 0 : multiplier > 1 ? 1 : multiplier; - if (isNaN(multiplier)) { - multiplier = 1; - } - - var colorStopEls = el.getElementsByTagName('stop'), - type, - gradientUnits = el.getAttribute('gradientUnits') === 'userSpaceOnUse' ? - 'pixels' : 'percentage', - gradientTransform = el.getAttribute('gradientTransform') || '', - colorStops = [], - coords, i, offsetX = 0, offsetY = 0, - transformMatrix; - if (el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT') { - type = 'linear'; - coords = getLinearCoords(el); - } - else { - type = 'radial'; - coords = getRadialCoords(el); - } - - for (i = colorStopEls.length; i--;) { - colorStops.push(getColorStop(colorStopEls[i], multiplier)); - } - - transformMatrix = fabric.parseTransformAttribute(gradientTransform); - - __convertPercentUnitsToValues(instance, coords, svgOptions, gradientUnits); - - if (gradientUnits === 'pixels') { - offsetX = -instance.left; - offsetY = -instance.top; - } - - var gradient = new fabric.Gradient({ - id: el.getAttribute('id'), - type: type, - coords: coords, - colorStops: colorStops, - gradientUnits: gradientUnits, - gradientTransform: transformMatrix, - offsetX: offsetX, - offsetY: offsetY, - }); - - return gradient; - } - /* _FROM_SVG_END_ */ - }); - - /** - * @private - */ - function __convertPercentUnitsToValues(instance, options, svgOptions, gradientUnits) { - var propValue, finalValue; - Object.keys(options).forEach(function (prop) { - propValue = options[prop]; - if (propValue === 'Infinity') { - finalValue = 1; - } - else if (propValue === '-Infinity') { - finalValue = 0; - } - else { - finalValue = parseFloat(options[prop], 10); - if (typeof propValue === 'string' && /^(\d+\.\d+)%|(\d+)%$/.test(propValue)) { - finalValue *= 0.01; - if (gradientUnits === 'pixels') { - // then we need to fix those percentages here in svg parsing - if (prop === 'x1' || prop === 'x2' || prop === 'r2') { - finalValue *= svgOptions.viewBoxWidth || svgOptions.width; - } - if (prop === 'y1' || prop === 'y2') { - finalValue *= svgOptions.viewBoxHeight || svgOptions.height; - } - } - } - } - options[prop] = finalValue; - }); - } -})(); - - -(function () { - - 'use strict'; - - var toFixed = fabric.util.toFixed; - - /** - * Pattern class - * @class fabric.Pattern - * @see {@link http://fabricjs.com/patterns|Pattern demo} - * @see {@link http://fabricjs.com/dynamic-patterns|DynamicPattern demo} - * @see {@link fabric.Pattern#initialize} for constructor definition - */ - - - fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { - - /** - * Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) - * @type String - * @default - */ - repeat: 'repeat', - - /** - * Pattern horizontal offset from object's left/top corner - * @type Number - * @default - */ - offsetX: 0, - - /** - * Pattern vertical offset from object's left/top corner - * @type Number - * @default - */ - offsetY: 0, - - /** - * crossOrigin value (one of "", "anonymous", "use-credentials") - * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes - * @type String - * @default - */ - crossOrigin: '', - - /** - * transform matrix to change the pattern, imported from svgs. - * @type Array - * @default - */ - patternTransform: null, - - /** - * Constructor - * @param {Object} [options] Options object - * @param {Function} [callback] function to invoke after callback init. - * @return {fabric.Pattern} thisArg - */ - initialize: function (options, callback) { - options || (options = {}); - - this.id = fabric.Object.__uid++; - this.setOptions(options); - if (!options.source || (options.source && typeof options.source !== 'string')) { - callback && callback(this); - return; - } - else { - // img src string - var _this = this; - this.source = fabric.util.createImage(); - fabric.util.loadImage(options.source, function (img, isError) { - _this.source = img; - callback && callback(_this, isError); - }, null, this.crossOrigin); - } - }, - - /** - * Returns object representation of a pattern - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of a pattern instance - */ - toObject: function (propertiesToInclude) { - var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, - source, object; - - // element - if (typeof this.source.src === 'string') { - source = this.source.src; - } - // element - else if (typeof this.source === 'object' && this.source.toDataURL) { - source = this.source.toDataURL(); - } - - object = { - type: 'pattern', - source: source, - repeat: this.repeat, - crossOrigin: this.crossOrigin, - offsetX: toFixed(this.offsetX, NUM_FRACTION_DIGITS), - offsetY: toFixed(this.offsetY, NUM_FRACTION_DIGITS), - patternTransform: this.patternTransform ? this.patternTransform.concat() : null - }; - fabric.util.populateWithProperties(this, object, propertiesToInclude); - - return object; - }, - - /* _TO_SVG_START_ */ - /** - * Returns SVG representation of a pattern - * @param {fabric.Object} object - * @return {String} SVG representation of a pattern - */ - toSVG: function (object) { - var patternSource = typeof this.source === 'function' ? this.source() : this.source, - patternWidth = patternSource.width / object.width, - patternHeight = patternSource.height / object.height, - patternOffsetX = this.offsetX / object.width, - patternOffsetY = this.offsetY / object.height, - patternImgSrc = ''; - if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { - patternHeight = 1; - if (patternOffsetY) { - patternHeight += Math.abs(patternOffsetY); - } - } - if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { - patternWidth = 1; - if (patternOffsetX) { - patternWidth += Math.abs(patternOffsetX); - } - - } - if (patternSource.src) { - patternImgSrc = patternSource.src; - } - else if (patternSource.toDataURL) { - patternImgSrc = patternSource.toDataURL(); - } - - return '\n' + - '\n' + - '\n'; - }, - /* _TO_SVG_END_ */ - - setOptions: function (options) { - for (var prop in options) { - this[prop] = options[prop]; - } - }, - - /** - * Returns an instance of CanvasPattern - * @param {CanvasRenderingContext2D} ctx Context to create pattern - * @return {CanvasPattern} - */ - toLive: function (ctx) { - var source = this.source; - // if the image failed to load, return, and allow rest to continue loading - if (!source) { - return ''; - } - - // if an image - if (typeof source.src !== 'undefined') { - if (!source.complete) { - return ''; - } - if (source.naturalWidth === 0 || source.naturalHeight === 0) { - return ''; - } - } - return ctx.createPattern(source, this.repeat); - } - }); -})(); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - toFixed = fabric.util.toFixed; - - if (fabric.Shadow) { - fabric.warn('fabric.Shadow is already defined.'); - return; - } - - /** - * Shadow class - * @class fabric.Shadow - * @see {@link http://fabricjs.com/shadows|Shadow demo} - * @see {@link fabric.Shadow#initialize} for constructor definition - */ - fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ { - - /** - * Shadow color - * @type String - * @default - */ - color: 'rgb(0,0,0)', - - /** - * Shadow blur - * @type Number - */ - blur: 0, - - /** - * Shadow horizontal offset - * @type Number - * @default - */ - offsetX: 0, - - /** - * Shadow vertical offset - * @type Number - * @default - */ - offsetY: 0, - - /** - * Whether the shadow should affect stroke operations - * @type Boolean - * @default - */ - affectStroke: false, - - /** - * Indicates whether toObject should include default values - * @type Boolean - * @default - */ - includeDefaultValues: true, - - /** - * When `false`, the shadow will scale with the object. - * When `true`, the shadow's offsetX, offsetY, and blur will not be affected by the object's scale. - * default to false - * @type Boolean - * @default - */ - nonScaling: false, - - /** - * Constructor - * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetY properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px") - * @return {fabric.Shadow} thisArg - */ - initialize: function (options) { - - if (typeof options === 'string') { - options = this._parseShadow(options); - } - - for (var prop in options) { - this[prop] = options[prop]; - } - - this.id = fabric.Object.__uid++; - }, - - /** - * @private - * @param {String} shadow Shadow value to parse - * @return {Object} Shadow object with color, offsetX, offsetY and blur - */ - _parseShadow: function (shadow) { - var shadowStr = shadow.trim(), - offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [], - color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)'; - - return { - color: color.trim(), - offsetX: parseFloat(offsetsAndBlur[1], 10) || 0, - offsetY: parseFloat(offsetsAndBlur[2], 10) || 0, - blur: parseFloat(offsetsAndBlur[3], 10) || 0 - }; - }, - - /** - * Returns a string representation of an instance - * @see http://www.w3.org/TR/css-text-decor-3/#text-shadow - * @return {String} Returns CSS3 text-shadow declaration - */ - toString: function () { - return [this.offsetX, this.offsetY, this.blur, this.color].join('px '); - }, - - /* _TO_SVG_START_ */ - /** - * Returns SVG representation of a shadow - * @param {fabric.Object} object - * @return {String} SVG representation of a shadow - */ - toSVG: function (object) { - var fBoxX = 40, fBoxY = 40, NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, - offset = fabric.util.rotateVector( - { x: this.offsetX, y: this.offsetY }, - fabric.util.degreesToRadians(-object.angle)), - BLUR_BOX = 20, color = new fabric.Color(this.color); - - if (object.width && object.height) { - //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion - // we add some extra space to filter box to contain the blur ( 20 ) - fBoxX = toFixed((Math.abs(offset.x) + this.blur) / object.width, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; - fBoxY = toFixed((Math.abs(offset.y) + this.blur) / object.height, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; - } - if (object.flipX) { - offset.x *= -1; - } - if (object.flipY) { - offset.y *= -1; - } - - return ( - '\n' + - '\t\n' + - '\t\n' + - '\t\n' + - '\t\n' + - '\t\n' + - '\t\t\n' + - '\t\t\n' + - '\t\n' + - '\n'); - }, - /* _TO_SVG_END_ */ - - /** - * Returns object representation of a shadow - * @return {Object} Object representation of a shadow instance - */ - toObject: function () { - if (this.includeDefaultValues) { - return { - color: this.color, - blur: this.blur, - offsetX: this.offsetX, - offsetY: this.offsetY, - affectStroke: this.affectStroke, - nonScaling: this.nonScaling - }; - } - var obj = {}, proto = fabric.Shadow.prototype; - - ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke', 'nonScaling'].forEach(function (prop) { - if (this[prop] !== proto[prop]) { - obj[prop] = this[prop]; - } - }, this); - - return obj; - } - }); - - /** - * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px") - * @static - * @field - * @memberOf fabric.Shadow - */ - // eslint-disable-next-line max-len - fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function () { - - 'use strict'; - - if (fabric.StaticCanvas) { - fabric.warn('fabric.StaticCanvas is already defined.'); - return; - } - - // aliases for faster resolution - var extend = fabric.util.object.extend, - getElementOffset = fabric.util.getElementOffset, - removeFromArray = fabric.util.removeFromArray, - toFixed = fabric.util.toFixed, - transformPoint = fabric.util.transformPoint, - invertTransform = fabric.util.invertTransform, - getNodeCanvas = fabric.util.getNodeCanvas, - createCanvasElement = fabric.util.createCanvasElement, - - CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); - - /** - * Static canvas class - * @class fabric.StaticCanvas - * @mixes fabric.Collection - * @mixes fabric.Observable - * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo} - * @see {@link fabric.StaticCanvas#initialize} for constructor definition - * @fires before:render - * @fires after:render - * @fires canvas:cleared - * @fires object:added - * @fires object:removed - */ - fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.StaticCanvas.prototype */ { - - /** - * Constructor - * @param {HTMLElement | String} el <canvas> element to initialize instance on - * @param {Object} [options] Options object - * @return {Object} thisArg - */ - initialize: function (el, options) { - options || (options = {}); - this.renderAndResetBound = this.renderAndReset.bind(this); - this.requestRenderAllBound = this.requestRenderAll.bind(this); - this._initStatic(el, options); - }, - - /** - * Background color of canvas instance. - * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. - * @type {(String|fabric.Pattern)} - * @default - */ - backgroundColor: '', - - /** - * Background image of canvas instance. - * since 2.4.0 image caching is active, please when putting an image as background, add to the - * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom - * vale. As an alternative you can disable image objectCaching - * @type fabric.Image - * @default - */ - backgroundImage: null, - - /** - * Overlay color of canvas instance. - * Should be set via {@link fabric.StaticCanvas#setOverlayColor} - * @since 1.3.9 - * @type {(String|fabric.Pattern)} - * @default - */ - overlayColor: '', - - /** - * Overlay image of canvas instance. - * since 2.4.0 image caching is active, please when putting an image as overlay, add to the - * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom - * vale. As an alternative you can disable image objectCaching - * @type fabric.Image - * @default - */ - overlayImage: null, - - /** - * Indicates whether toObject/toDatalessObject should include default values - * if set to false, takes precedence over the object value. - * @type Boolean - * @default - */ - includeDefaultValues: true, - - /** - * Indicates whether objects' state should be saved - * @type Boolean - * @default - */ - stateful: false, - - /** - * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove}, - * {@link fabric.StaticCanvas.moveTo}, {@link fabric.StaticCanvas.clear} and many more, should also re-render canvas. - * Disabling this option will not give a performance boost when adding/removing a lot of objects to/from canvas at once - * since the renders are quequed and executed one per frame. - * Disabling is suggested anyway and managing the renders of the app manually is not a big effort ( canvas.requestRenderAll() ) - * Left default to true to do not break documentation and old app, fiddles. - * @type Boolean - * @default - */ - renderOnAddRemove: true, - - /** - * Indicates whether object controls (borders/controls) are rendered above overlay image - * @type Boolean - * @default - */ - controlsAboveOverlay: false, - - /** - * Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas - * @type Boolean - * @default - */ - allowTouchScrolling: false, - - /** - * Indicates whether this canvas will use image smoothing, this is on by default in browsers - * @type Boolean - * @default - */ - imageSmoothingEnabled: true, - - /** - * The transformation (a Canvas 2D API transform matrix) which focuses the viewport - * @type Array - * @example Default transform - * canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; - * @example Scale by 70% and translate toward bottom-right by 50, without skewing - * canvas.viewportTransform = [0.7, 0, 0, 0.7, 50, 50]; - * @default - */ - viewportTransform: fabric.iMatrix.concat(), - - /** - * if set to false background image is not affected by viewport transform - * @since 1.6.3 - * @type Boolean - * @default - */ - backgroundVpt: true, - - /** - * if set to false overlya image is not affected by viewport transform - * @since 1.6.3 - * @type Boolean - * @default - */ - overlayVpt: true, - - /** - * When true, canvas is scaled by devicePixelRatio for better rendering on retina screens - * @type Boolean - * @default - */ - enableRetinaScaling: true, - - /** - * Describe canvas element extension over design - * properties are tl,tr,bl,br. - * if canvas is not zoomed/panned those points are the four corner of canvas - * if canvas is viewportTransformed you those points indicate the extension - * of canvas element in plain untrasformed coordinates - * The coordinates get updated with @method calcViewportBoundaries. - * @memberOf fabric.StaticCanvas.prototype - */ - vptCoords: {}, - - /** - * Based on vptCoords and object.aCoords, skip rendering of objects that - * are not included in current viewport. - * May greatly help in applications with crowded canvas and use of zoom/pan - * If One of the corner of the bounding box of the object is on the canvas - * the objects get rendered. - * @memberOf fabric.StaticCanvas.prototype - * @type Boolean - * @default - */ - skipOffscreen: true, - - /** - * a fabricObject that, without stroke define a clipping area with their shape. filled in black - * the clipPath object gets used when the canvas has rendered, and the context is placed in the - * top left corner of the canvas. - * clipPath will clip away controls, if you do not want this to happen use controlsAboveOverlay = true - * @type fabric.Object - */ - clipPath: undefined, - - /** - * @private - * @param {HTMLElement | String} el <canvas> element to initialize instance on - * @param {Object} [options] Options object - */ - _initStatic: function (el, options) { - var cb = this.requestRenderAllBound; - this._objects = []; - this._createLowerCanvas(el); - this._initOptions(options); - // only initialize retina scaling once - if (!this.interactive) { - this._initRetinaScaling(); - } - - if (options.overlayImage) { - this.setOverlayImage(options.overlayImage, cb); - } - if (options.backgroundImage) { - this.setBackgroundImage(options.backgroundImage, cb); - } - if (options.backgroundColor) { - this.setBackgroundColor(options.backgroundColor, cb); - } - if (options.overlayColor) { - this.setOverlayColor(options.overlayColor, cb); - } - this.calcOffset(); - }, - - /** - * @private - */ - _isRetinaScaling: function () { - return (fabric.devicePixelRatio > 1 && this.enableRetinaScaling); - }, - - /** - * @private - * @return {Number} retinaScaling if applied, otherwise 1; - */ - getRetinaScaling: function () { - return this._isRetinaScaling() ? Math.max(1, fabric.devicePixelRatio) : 1; - }, - - /** - * @private - */ - _initRetinaScaling: function () { - if (!this._isRetinaScaling()) { - return; - } - var scaleRatio = fabric.devicePixelRatio; - this.__initRetinaScaling(scaleRatio, this.lowerCanvasEl, this.contextContainer); - if (this.upperCanvasEl) { - this.__initRetinaScaling(scaleRatio, this.upperCanvasEl, this.contextTop); - } - }, - - __initRetinaScaling: function (scaleRatio, canvas, context) { - canvas.setAttribute('width', this.width * scaleRatio); - canvas.setAttribute('height', this.height * scaleRatio); - context.scale(scaleRatio, scaleRatio); - }, - - - /** - * Calculates canvas element offset relative to the document - * This method is also attached as "resize" event handler of window - * @return {fabric.Canvas} instance - * @chainable - */ - calcOffset: function () { - this._offset = getElementOffset(this.lowerCanvasEl); - return this; - }, - - /** - * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas - * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to - * @param {Function} callback callback to invoke when image is loaded and set as an overlay - * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} - * @example Normal overlayImage with left/top = 0 - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * // Needed to position overlayImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example overlayImage with different properties - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top' - * }); - * @example Stretched overlayImage #1 - width/height correspond to canvas width/height - * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img, isError) { - * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); - * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); - * }); - * @example Stretched overlayImage #2 - width/height correspond to canvas width/height - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * width: canvas.width, - * height: canvas.height, - * // Needed to position overlayImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example overlayImage loaded from cross-origin - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top', - * crossOrigin: 'anonymous' - * }); - */ - setOverlayImage: function (image, callback, options) { - return this.__setBgOverlayImage('overlayImage', image, callback, options); - }, - - /** - * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas - * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to - * @param {Function} callback Callback to invoke when image is loaded and set as background - * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/djnr8o7a/28/|jsFiddle demo} - * @example Normal backgroundImage with left/top = 0 - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * // Needed to position backgroundImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example backgroundImage with different properties - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top' - * }); - * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height - * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img, isError) { - * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); - * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); - * }); - * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * width: canvas.width, - * height: canvas.height, - * // Needed to position backgroundImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example backgroundImage loaded from cross-origin - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top', - * crossOrigin: 'anonymous' - * }); - */ - // TODO: fix stretched examples - setBackgroundImage: function (image, callback, options) { - return this.__setBgOverlayImage('backgroundImage', image, callback, options); - }, - - /** - * Sets {@link fabric.StaticCanvas#overlayColor|foreground color} for this canvas - * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set foreground color to - * @param {Function} callback Callback to invoke when foreground color is set - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} - * @example Normal overlayColor - color value - * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as overlayColor - * canvas.setOverlayColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png' - * }, canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as overlayColor with repeat and offset - * canvas.setOverlayColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png', - * repeat: 'repeat', - * offsetX: 200, - * offsetY: 100 - * }, canvas.renderAll.bind(canvas)); - */ - setOverlayColor: function (overlayColor, callback) { - return this.__setBgOverlayColor('overlayColor', overlayColor, callback); - }, - - /** - * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas - * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to - * @param {Function} callback Callback to invoke when background color is set - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} - * @example Normal backgroundColor - color value - * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as backgroundColor - * canvas.setBackgroundColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png' - * }, canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as backgroundColor with repeat and offset - * canvas.setBackgroundColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png', - * repeat: 'repeat', - * offsetX: 200, - * offsetY: 100 - * }, canvas.renderAll.bind(canvas)); - */ - setBackgroundColor: function (backgroundColor, callback) { - return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); - }, - - /** - * @private - * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} - * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) - * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to - * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not. - * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. - */ - __setBgOverlayImage: function (property, image, callback, options) { - if (typeof image === 'string') { - fabric.util.loadImage(image, function (img, isError) { - if (img) { - var instance = new fabric.Image(img, options); - this[property] = instance; - instance.canvas = this; - } - callback && callback(img, isError); - }, this, options && options.crossOrigin); - } - else { - options && image.setOptions(options); - this[property] = image; - image && (image.canvas = this); - callback && callback(image, false); - } - - return this; - }, - - /** - * @private - * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} - * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) - * @param {(Object|String|null)} color Object with pattern information, color value or null - * @param {Function} [callback] Callback is invoked when color is set - */ - __setBgOverlayColor: function (property, color, callback) { - this[property] = color; - this._initGradient(color, property); - this._initPattern(color, property, callback); - return this; - }, - - /** - * @private - */ - _createCanvasElement: function () { - var element = createCanvasElement(); - if (!element) { - throw CANVAS_INIT_ERROR; - } - if (!element.style) { - element.style = {}; - } - if (typeof element.getContext === 'undefined') { - throw CANVAS_INIT_ERROR; - } - return element; - }, - - /** - * @private - * @param {Object} [options] Options object - */ - _initOptions: function (options) { - var lowerCanvasEl = this.lowerCanvasEl; - this._setOptions(options); - - this.width = this.width || parseInt(lowerCanvasEl.width, 10) || 0; - this.height = this.height || parseInt(lowerCanvasEl.height, 10) || 0; - - if (!this.lowerCanvasEl.style) { - return; - } - - lowerCanvasEl.width = this.width; - lowerCanvasEl.height = this.height; - - lowerCanvasEl.style.width = this.width + 'px'; - lowerCanvasEl.style.height = this.height + 'px'; - - this.viewportTransform = this.viewportTransform.slice(); - }, - - /** - * Creates a bottom canvas - * @private - * @param {HTMLElement} [canvasEl] - */ - _createLowerCanvas: function (canvasEl) { - // canvasEl === 'HTMLCanvasElement' does not work on jsdom/node - if (canvasEl && canvasEl.getContext) { - this.lowerCanvasEl = canvasEl; - } - else { - this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); - } - - fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); - this._originalCanvasStyle = this.lowerCanvasEl.style; - if (this.interactive) { - this._applyCanvasStyle(this.lowerCanvasEl); - } - - this.contextContainer = this.lowerCanvasEl.getContext('2d'); - }, - - /** - * Returns canvas width (in px) - * @return {Number} - */ - getWidth: function () { - return this.width; - }, - - /** - * Returns canvas height (in px) - * @return {Number} - */ - getHeight: function () { - return this.height; - }, - - /** - * Sets width of this canvas instance - * @param {Number|String} value Value to set width to - * @param {Object} [options] Options object - * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions - * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions - * @return {fabric.Canvas} instance - * @chainable true - */ - setWidth: function (value, options) { - return this.setDimensions({ width: value }, options); - }, - - /** - * Sets height of this canvas instance - * @param {Number|String} value Value to set height to - * @param {Object} [options] Options object - * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions - * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions - * @return {fabric.Canvas} instance - * @chainable true - */ - setHeight: function (value, options) { - return this.setDimensions({ height: value }, options); - }, - - /** - * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em) - * @param {Object} dimensions Object with width/height properties - * @param {Number|String} [dimensions.width] Width of canvas element - * @param {Number|String} [dimensions.height] Height of canvas element - * @param {Object} [options] Options object - * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions - * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions - * @return {fabric.Canvas} thisArg - * @chainable - */ - setDimensions: function (dimensions, options) { - var cssValue; - - options = options || {}; - - for (var prop in dimensions) { - cssValue = dimensions[prop]; - - if (!options.cssOnly) { - this._setBackstoreDimension(prop, dimensions[prop]); - cssValue += 'px'; - this.hasLostContext = true; - } - - if (!options.backstoreOnly) { - this._setCssDimension(prop, cssValue); - } - } - if (this._isCurrentlyDrawing) { - this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(this.contextTop); - } - this._initRetinaScaling(); - this.calcOffset(); - - if (!options.cssOnly) { - this.requestRenderAll(); - } - - return this; - }, - - /** - * Helper for setting width/height - * @private - * @param {String} prop property (width|height) - * @param {Number} value value to set property to - * @return {fabric.Canvas} instance - * @chainable true - */ - _setBackstoreDimension: function (prop, value) { - this.lowerCanvasEl[prop] = value; - - if (this.upperCanvasEl) { - this.upperCanvasEl[prop] = value; - } - - if (this.cacheCanvasEl) { - this.cacheCanvasEl[prop] = value; - } - - this[prop] = value; - - return this; - }, - - /** - * Helper for setting css width/height - * @private - * @param {String} prop property (width|height) - * @param {String} value value to set property to - * @return {fabric.Canvas} instance - * @chainable true - */ - _setCssDimension: function (prop, value) { - this.lowerCanvasEl.style[prop] = value; - - if (this.upperCanvasEl) { - this.upperCanvasEl.style[prop] = value; - } - - if (this.wrapperEl) { - this.wrapperEl.style[prop] = value; - } - - return this; - }, - - /** - * Returns canvas zoom level - * @return {Number} - */ - getZoom: function () { - return this.viewportTransform[0]; - }, - - /** - * Sets viewport transformation of this canvas instance - * @param {Array} vpt a Canvas 2D API transform matrix - * @return {fabric.Canvas} instance - * @chainable true - */ - setViewportTransform: function (vpt) { - var activeObject = this._activeObject, - backgroundObject = this.backgroundImage, - overlayObject = this.overlayImage, - object, i, len; - this.viewportTransform = vpt; - for (i = 0, len = this._objects.length; i < len; i++) { - object = this._objects[i]; - object.group || object.setCoords(true); - } - if (activeObject) { - activeObject.setCoords(); - } - if (backgroundObject) { - backgroundObject.setCoords(true); - } - if (overlayObject) { - overlayObject.setCoords(true); - } - this.calcViewportBoundaries(); - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Sets zoom level of this canvas instance, the zoom centered around point - * meaning that following zoom to point with the same point will have the visual - * effect of the zoom originating from that point. The point won't move. - * It has nothing to do with canvas center or visual center of the viewport. - * @param {fabric.Point} point to zoom with respect to - * @param {Number} value to set zoom to, less than 1 zooms out - * @return {fabric.Canvas} instance - * @chainable true - */ - zoomToPoint: function (point, value) { - // TODO: just change the scale, preserve other transformations - var before = point, vpt = this.viewportTransform.slice(0); - point = transformPoint(point, invertTransform(this.viewportTransform)); - vpt[0] = value; - vpt[3] = value; - var after = transformPoint(point, vpt); - vpt[4] += before.x - after.x; - vpt[5] += before.y - after.y; - return this.setViewportTransform(vpt); - }, - - /** - * Sets zoom level of this canvas instance - * @param {Number} value to set zoom to, less than 1 zooms out - * @return {fabric.Canvas} instance - * @chainable true - */ - setZoom: function (value) { - this.zoomToPoint(new fabric.Point(0, 0), value); - return this; - }, - - /** - * Pan viewport so as to place point at top left corner of canvas - * @param {fabric.Point} point to move to - * @return {fabric.Canvas} instance - * @chainable true - */ - absolutePan: function (point) { - var vpt = this.viewportTransform.slice(0); - vpt[4] = -point.x; - vpt[5] = -point.y; - return this.setViewportTransform(vpt); - }, - - /** - * Pans viewpoint relatively - * @param {fabric.Point} point (position vector) to move by - * @return {fabric.Canvas} instance - * @chainable true - */ - relativePan: function (point) { - return this.absolutePan(new fabric.Point( - -point.x - this.viewportTransform[4], - -point.y - this.viewportTransform[5] - )); - }, - - /** - * Returns <canvas> element corresponding to this instance - * @return {HTMLCanvasElement} - */ - getElement: function () { - return this.lowerCanvasEl; - }, - - /** - * @private - * @param {fabric.Object} obj Object that was added - */ - _onObjectAdded: function (obj) { - this.stateful && obj.setupState(); - obj._set('canvas', this); - obj.setCoords(); - this.fire('object:added', { target: obj }); - obj.fire('added'); - }, - - /** - * @private - * @param {fabric.Object} obj Object that was removed - */ - _onObjectRemoved: function (obj) { - this.fire('object:removed', { target: obj }); - obj.fire('removed'); - delete obj.canvas; - }, - - /** - * Clears specified context of canvas element - * @param {CanvasRenderingContext2D} ctx Context to clear - * @return {fabric.Canvas} thisArg - * @chainable - */ - clearContext: function (ctx) { - ctx.clearRect(0, 0, this.width, this.height); - return this; - }, - - /** - * Returns context of canvas where objects are drawn - * @return {CanvasRenderingContext2D} - */ - getContext: function () { - return this.contextContainer; - }, - - /** - * Clears all contexts (background, main, top) of an instance - * @return {fabric.Canvas} thisArg - * @chainable - */ - clear: function () { - this.remove.apply(this, this.getObjects()); - this.backgroundImage = null; - this.overlayImage = null; - this.backgroundColor = ''; - this.overlayColor = ''; - if (this._hasITextHandlers) { - this.off('mouse:up', this._mouseUpITextHandler); - this._iTextInstances = null; - this._hasITextHandlers = false; - } - this.clearContext(this.contextContainer); - this.fire('canvas:cleared'); - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Renders the canvas - * @return {fabric.Canvas} instance - * @chainable - */ - renderAll: function () { - var canvasToDrawOn = this.contextContainer; - this.renderCanvas(canvasToDrawOn, this._objects); - return this; - }, - - /** - * Function created to be instance bound at initialization - * used in requestAnimationFrame rendering - * Let the fabricJS call it. If you call it manually you could have more - * animationFrame stacking on to of each other - * for an imperative rendering, use canvas.renderAll - * @private - * @return {fabric.Canvas} instance - * @chainable - */ - renderAndReset: function () { - this.isRendering = 0; - this.renderAll(); - }, - - /** - * Append a renderAll request to next animation frame. - * unless one is already in progress, in that case nothing is done - * a boolean flag will avoid appending more. - * @return {fabric.Canvas} instance - * @chainable - */ - requestRenderAll: function () { - if (!this.isRendering) { - this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound); - } - return this; - }, - - /** - * Calculate the position of the 4 corner of canvas with current viewportTransform. - * helps to determinate when an object is in the current rendering viewport using - * object absolute coordinates ( aCoords ) - * @return {Object} points.tl - * @chainable - */ - calcViewportBoundaries: function () { - var points = {}, width = this.width, height = this.height, - iVpt = invertTransform(this.viewportTransform); - points.tl = transformPoint({ x: 0, y: 0 }, iVpt); - points.br = transformPoint({ x: width, y: height }, iVpt); - points.tr = new fabric.Point(points.br.x, points.tl.y); - points.bl = new fabric.Point(points.tl.x, points.br.y); - this.vptCoords = points; - return points; - }, - - cancelRequestedRender: function () { - if (this.isRendering) { - fabric.util.cancelAnimFrame(this.isRendering); - this.isRendering = 0; - } - }, - - /** - * Renders background, objects, overlay and controls. - * @param {CanvasRenderingContext2D} ctx - * @param {Array} objects to render - * @return {fabric.Canvas} instance - * @chainable - */ - renderCanvas: function (ctx, objects) { - var v = this.viewportTransform, path = this.clipPath; - this.cancelRequestedRender(); - this.calcViewportBoundaries(); - this.clearContext(ctx); - fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled); - this.fire('before:render', { ctx: ctx, }); - this._renderBackground(ctx); - - ctx.save(); - //apply viewport transform once for all rendering process - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - this._renderObjects(ctx, objects); - ctx.restore(); - if (!this.controlsAboveOverlay && this.interactive) { - this.drawControls(ctx); - } - if (path) { - path.canvas = this; - // needed to setup a couple of variables - path.shouldCache(); - path._transformDone = true; - path.renderCache({ forClipping: true }); - this.drawClipPathOnCanvas(ctx); - } - this._renderOverlay(ctx); - if (this.controlsAboveOverlay && this.interactive) { - this.drawControls(ctx); - } - this.fire('after:render', { ctx: ctx, }); - }, - - /** - * Paint the cached clipPath on the lowerCanvasEl - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - drawClipPathOnCanvas: function (ctx) { - var v = this.viewportTransform, path = this.clipPath; - ctx.save(); - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - // DEBUG: uncomment this line, comment the following - // ctx.globalAlpha = 0.4; - ctx.globalCompositeOperation = 'destination-in'; - path.transform(ctx); - ctx.scale(1 / path.zoomX, 1 / path.zoomY); - ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY); - ctx.restore(); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} objects to render - */ - _renderObjects: function (ctx, objects) { - var i, len; - for (i = 0, len = objects.length; i < len; ++i) { - objects[i] && objects[i].render(ctx); - } - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {string} property 'background' or 'overlay' - */ - _renderBackgroundOrOverlay: function (ctx, property) { - var fill = this[property + 'Color'], object = this[property + 'Image'], - v = this.viewportTransform, needsVpt = this[property + 'Vpt']; - if (!fill && !object) { - return; - } - if (fill) { - ctx.save(); - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(this.width, 0); - ctx.lineTo(this.width, this.height); - ctx.lineTo(0, this.height); - ctx.closePath(); - ctx.fillStyle = fill.toLive - ? fill.toLive(ctx, this) - : fill; - if (needsVpt) { - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - } - ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0); - var m = fill.gradientTransform || fill.patternTransform; - m && ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - ctx.fill(); - ctx.restore(); - } - if (object) { - ctx.save(); - if (needsVpt) { - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - } - object.render(ctx); - ctx.restore(); - } - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderBackground: function (ctx) { - this._renderBackgroundOrOverlay(ctx, 'background'); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderOverlay: function (ctx) { - this._renderBackgroundOrOverlay(ctx, 'overlay'); - }, - - /** - * Returns coordinates of a center of canvas. - * Returned value is an object with top and left properties - * @return {Object} object with "top" and "left" number values - * @deprecated migrate to `getCenterPoint` - */ - getCenter: function () { - return { - top: this.height / 2, - left: this.width / 2 - }; - }, - - /** - * Returns coordinates of a center of canvas. - * @return {fabric.Point} - */ - getCenterPoint: function () { - return new fabric.Point(this.width / 2, this.height / 2); - }, - - /** - * Centers object horizontally in the canvas - * @param {fabric.Object} object Object to center horizontally - * @return {fabric.Canvas} thisArg - */ - centerObjectH: function (object) { - return this._centerObject(object, new fabric.Point(this.getCenterPoint().x, object.getCenterPoint().y)); - }, - - /** - * Centers object vertically in the canvas - * @param {fabric.Object} object Object to center vertically - * @return {fabric.Canvas} thisArg - * @chainable - */ - centerObjectV: function (object) { - return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenterPoint().y)); - }, - - /** - * Centers object vertically and horizontally in the canvas - * @param {fabric.Object} object Object to center vertically and horizontally - * @return {fabric.Canvas} thisArg - * @chainable - */ - centerObject: function (object) { - var center = this.getCenterPoint(); - return this._centerObject(object, center); - }, - - /** - * Centers object vertically and horizontally in the viewport - * @param {fabric.Object} object Object to center vertically and horizontally - * @return {fabric.Canvas} thisArg - * @chainable - */ - viewportCenterObject: function (object) { - var vpCenter = this.getVpCenter(); - return this._centerObject(object, vpCenter); - }, - - /** - * Centers object horizontally in the viewport, object.top is unchanged - * @param {fabric.Object} object Object to center vertically and horizontally - * @return {fabric.Canvas} thisArg - * @chainable - */ - viewportCenterObjectH: function (object) { - var vpCenter = this.getVpCenter(); - this._centerObject(object, new fabric.Point(vpCenter.x, object.getCenterPoint().y)); - return this; - }, - - /** - * Centers object Vertically in the viewport, object.top is unchanged - * @param {fabric.Object} object Object to center vertically and horizontally - * @return {fabric.Canvas} thisArg - * @chainable - */ - viewportCenterObjectV: function (object) { - var vpCenter = this.getVpCenter(); - - return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, vpCenter.y)); - }, - - /** - * Calculate the point in canvas that correspond to the center of actual viewport. - * @return {fabric.Point} vpCenter, viewport center - * @chainable - */ - getVpCenter: function () { - var center = this.getCenterPoint(), - iVpt = invertTransform(this.viewportTransform); - return transformPoint(center, iVpt); - }, - - /** - * @private - * @param {fabric.Object} object Object to center - * @param {fabric.Point} center Center point - * @return {fabric.Canvas} thisArg - * @chainable - */ - _centerObject: function (object, center) { - object.setPositionByOrigin(center, 'center', 'center'); - object.setCoords(); - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Returns dataless JSON representation of canvas - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {String} json string - */ - toDatalessJSON: function (propertiesToInclude) { - return this.toDatalessObject(propertiesToInclude); - }, - - /** - * Returns object representation of canvas - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - return this._toObjectMethod('toObject', propertiesToInclude); - }, - - /** - * Returns dataless object representation of canvas - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toDatalessObject: function (propertiesToInclude) { - return this._toObjectMethod('toDatalessObject', propertiesToInclude); - }, - - /** - * @private - */ - _toObjectMethod: function (methodName, propertiesToInclude) { - - var clipPath = this.clipPath, data = { - version: fabric.version, - objects: this._toObjects(methodName, propertiesToInclude), - }; - if (clipPath && !clipPath.excludeFromExport) { - data.clipPath = this._toObject(this.clipPath, methodName, propertiesToInclude); - } - extend(data, this.__serializeBgOverlay(methodName, propertiesToInclude)); - - fabric.util.populateWithProperties(this, data, propertiesToInclude); - - return data; - }, - - /** - * @private - */ - _toObjects: function (methodName, propertiesToInclude) { - return this._objects.filter(function (object) { - return !object.excludeFromExport; - }).map(function (instance) { - return this._toObject(instance, methodName, propertiesToInclude); - }, this); - }, - - /** - * @private - */ - _toObject: function (instance, methodName, propertiesToInclude) { - var originalValue; - - if (!this.includeDefaultValues) { - originalValue = instance.includeDefaultValues; - instance.includeDefaultValues = false; - } - - var object = instance[methodName](propertiesToInclude); - if (!this.includeDefaultValues) { - instance.includeDefaultValues = originalValue; - } - return object; - }, - - /** - * @private - */ - __serializeBgOverlay: function (methodName, propertiesToInclude) { - var data = {}, bgImage = this.backgroundImage, overlayImage = this.overlayImage, - bgColor = this.backgroundColor, overlayColor = this.overlayColor; - - if (bgColor && bgColor.toObject) { - if (!bgColor.excludeFromExport) { - data.background = bgColor.toObject(propertiesToInclude); - } - } - else if (bgColor) { - data.background = bgColor; - } - - if (overlayColor && overlayColor.toObject) { - if (!overlayColor.excludeFromExport) { - data.overlay = overlayColor.toObject(propertiesToInclude); - } - } - else if (overlayColor) { - data.overlay = overlayColor; - } - - if (bgImage && !bgImage.excludeFromExport) { - data.backgroundImage = this._toObject(bgImage, methodName, propertiesToInclude); - } - if (overlayImage && !overlayImage.excludeFromExport) { - data.overlayImage = this._toObject(overlayImage, methodName, propertiesToInclude); - } - - return data; - }, - - /* _TO_SVG_START_ */ - /** - * When true, getSvgTransform() will apply the StaticCanvas.viewportTransform to the SVG transformation. When true, - * a zoomed canvas will then produce zoomed SVG output. - * @type Boolean - * @default - */ - svgViewportTransformation: true, - - /** - * Returns SVG representation of canvas - * @function - * @param {Object} [options] Options object for SVG output - * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included - * @param {Object} [options.viewBox] SVG viewbox object - * @param {Number} [options.viewBox.x] x-coordinate of viewbox - * @param {Number} [options.viewBox.y] y-coordinate of viewbox - * @param {Number} [options.viewBox.width] Width of viewbox - * @param {Number} [options.viewBox.height] Height of viewbox - * @param {String} [options.encoding=UTF-8] Encoding of SVG output - * @param {String} [options.width] desired width of svg with or without units - * @param {String} [options.height] desired height of svg with or without units - * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation. - * @return {String} SVG string - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} - * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo} - * @example Normal SVG output - * var svg = canvas.toSVG(); - * @example SVG output without preamble (without <?xml ../>) - * var svg = canvas.toSVG({suppressPreamble: true}); - * @example SVG output with viewBox attribute - * var svg = canvas.toSVG({ - * viewBox: { - * x: 100, - * y: 100, - * width: 200, - * height: 300 - * } - * }); - * @example SVG output with different encoding (default: UTF-8) - * var svg = canvas.toSVG({encoding: 'ISO-8859-1'}); - * @example Modify SVG output with reviver function - * var svg = canvas.toSVG(null, function(svg) { - * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); - * }); - */ - toSVG: function (options, reviver) { - options || (options = {}); - options.reviver = reviver; - var markup = []; - - this._setSVGPreamble(markup, options); - this._setSVGHeader(markup, options); - if (this.clipPath) { - markup.push('\n'); - } - this._setSVGBgOverlayColor(markup, 'background'); - this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); - this._setSVGObjects(markup, reviver); - if (this.clipPath) { - markup.push('\n'); - } - this._setSVGBgOverlayColor(markup, 'overlay'); - this._setSVGBgOverlayImage(markup, 'overlayImage', reviver); - - markup.push(''); - - return markup.join(''); - }, - - /** - * @private - */ - _setSVGPreamble: function (markup, options) { - if (options.suppressPreamble) { - return; - } - markup.push( - '\n', - '\n' - ); - }, - - /** - * @private - */ - _setSVGHeader: function (markup, options) { - var width = options.width || this.width, - height = options.height || this.height, - vpt, viewBox = 'viewBox="0 0 ' + this.width + ' ' + this.height + '" ', - NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - - if (options.viewBox) { - viewBox = 'viewBox="' + - options.viewBox.x + ' ' + - options.viewBox.y + ' ' + - options.viewBox.width + ' ' + - options.viewBox.height + '" '; - } - else { - if (this.svgViewportTransformation) { - vpt = this.viewportTransform; - viewBox = 'viewBox="' + - toFixed(-vpt[4] / vpt[0], NUM_FRACTION_DIGITS) + ' ' + - toFixed(-vpt[5] / vpt[3], NUM_FRACTION_DIGITS) + ' ' + - toFixed(this.width / vpt[0], NUM_FRACTION_DIGITS) + ' ' + - toFixed(this.height / vpt[3], NUM_FRACTION_DIGITS) + '" '; - } - } - - markup.push( - '\n', - 'Created with Fabric.js ', fabric.version, '\n', - '\n', - this.createSVGFontFacesMarkup(), - this.createSVGRefElementsMarkup(), - this.createSVGClipPathMarkup(options), - '\n' - ); - }, - - createSVGClipPathMarkup: function (options) { - var clipPath = this.clipPath; - if (clipPath) { - clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; - return '\n' + - this.clipPath.toClipPathSVG(options.reviver) + - '\n'; - } - return ''; - }, - - /** - * Creates markup containing SVG referenced elements like patterns, gradients etc. - * @return {String} - */ - createSVGRefElementsMarkup: function () { - var _this = this, - markup = ['background', 'overlay'].map(function (prop) { - var fill = _this[prop + 'Color']; - if (fill && fill.toLive) { - var shouldTransform = _this[prop + 'Vpt'], vpt = _this.viewportTransform, - object = { - width: _this.width / (shouldTransform ? vpt[0] : 1), - height: _this.height / (shouldTransform ? vpt[3] : 1) - }; - return fill.toSVG( - object, - { additionalTransform: shouldTransform ? fabric.util.matrixToSVG(vpt) : '' } - ); - } - }); - return markup.join(''); - }, - - /** - * Creates markup containing SVG font faces, - * font URLs for font faces must be collected by developers - * and are not extracted from the DOM by fabricjs - * @param {Array} objects Array of fabric objects - * @return {String} - */ - createSVGFontFacesMarkup: function () { - var markup = '', fontList = {}, obj, fontFamily, - style, row, rowIndex, _char, charIndex, i, len, - fontPaths = fabric.fontPaths, objects = []; - - this._objects.forEach(function add(object) { - objects.push(object); - if (object._objects) { - object._objects.forEach(add); - } - }); - - for (i = 0, len = objects.length; i < len; i++) { - obj = objects[i]; - fontFamily = obj.fontFamily; - if (obj.type.indexOf('text') === -1 || fontList[fontFamily] || !fontPaths[fontFamily]) { - continue; - } - fontList[fontFamily] = true; - if (!obj.styles) { - continue; - } - style = obj.styles; - for (rowIndex in style) { - row = style[rowIndex]; - for (charIndex in row) { - _char = row[charIndex]; - fontFamily = _char.fontFamily; - if (!fontList[fontFamily] && fontPaths[fontFamily]) { - fontList[fontFamily] = true; - } - } - } - } - - for (var j in fontList) { - markup += [ - '\t\t@font-face {\n', - '\t\t\tfont-family: \'', j, '\';\n', - '\t\t\tsrc: url(\'', fontPaths[j], '\');\n', - '\t\t}\n' - ].join(''); - } - - if (markup) { - markup = [ - '\t\n' - ].join(''); - } - - return markup; - }, - - /** - * @private - */ - _setSVGObjects: function (markup, reviver) { - var instance, i, len, objects = this._objects; - for (i = 0, len = objects.length; i < len; i++) { - instance = objects[i]; - if (instance.excludeFromExport) { - continue; - } - this._setSVGObject(markup, instance, reviver); - } - }, - - /** - * @private - */ - _setSVGObject: function (markup, instance, reviver) { - markup.push(instance.toSVG(reviver)); - }, - - /** - * @private - */ - _setSVGBgOverlayImage: function (markup, property, reviver) { - if (this[property] && !this[property].excludeFromExport && this[property].toSVG) { - markup.push(this[property].toSVG(reviver)); - } - }, - - /** - * @private - */ - _setSVGBgOverlayColor: function (markup, property) { - var filler = this[property + 'Color'], vpt = this.viewportTransform, finalWidth = this.width, - finalHeight = this.height; - if (!filler) { - return; - } - if (filler.toLive) { - var repeat = filler.repeat, iVpt = fabric.util.invertTransform(vpt), shouldInvert = this[property + 'Vpt'], - additionalTransform = shouldInvert ? fabric.util.matrixToSVG(iVpt) : ''; - markup.push( - '\n' - ); - } - else { - markup.push( - '\n' - ); - } - }, - /* _TO_SVG_END_ */ - - /** - * Moves an object or the objects of a multiple selection - * to the bottom of the stack of drawn objects - * @param {fabric.Object} object Object to send to back - * @return {fabric.Canvas} thisArg - * @chainable - */ - sendToBack: function (object) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, obj, objs; - if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = objs.length; i--;) { - obj = objs[i]; - removeFromArray(this._objects, obj); - this._objects.unshift(obj); - } - } - else { - removeFromArray(this._objects, object); - this._objects.unshift(object); - } - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Moves an object or the objects of a multiple selection - * to the top of the stack of drawn objects - * @param {fabric.Object} object Object to send - * @return {fabric.Canvas} thisArg - * @chainable - */ - bringToFront: function (object) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, obj, objs; - if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = 0; i < objs.length; i++) { - obj = objs[i]; - removeFromArray(this._objects, obj); - this._objects.push(obj); - } - } - else { - removeFromArray(this._objects, object); - this._objects.push(object); - } - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Moves an object or a selection down in stack of drawn objects - * An optional parameter, intersecting allows to move the object in behind - * the first intersecting object. Where intersection is calculated with - * bounding box. If no intersection is found, there will not be change in the - * stack. - * @param {fabric.Object} object Object to send - * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object - * @return {fabric.Canvas} thisArg - * @chainable - */ - sendBackwards: function (object, intersecting) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, obj, idx, newIdx, objs, objsMoved = 0; - - if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = 0; i < objs.length; i++) { - obj = objs[i]; - idx = this._objects.indexOf(obj); - if (idx > 0 + objsMoved) { - newIdx = idx - 1; - removeFromArray(this._objects, obj); - this._objects.splice(newIdx, 0, obj); - } - objsMoved++; - } - } - else { - idx = this._objects.indexOf(object); - if (idx !== 0) { - // if object is not on the bottom of stack - newIdx = this._findNewLowerIndex(object, idx, intersecting); - removeFromArray(this._objects, object); - this._objects.splice(newIdx, 0, object); - } - } - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * @private - */ - _findNewLowerIndex: function (object, idx, intersecting) { - var newIdx, i; - - if (intersecting) { - newIdx = idx; - - // traverse down the stack looking for the nearest intersecting object - for (i = idx - 1; i >= 0; --i) { - - var isIntersecting = object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } - else { - newIdx = idx - 1; - } - - return newIdx; - }, - - /** - * Moves an object or a selection up in stack of drawn objects - * An optional parameter, intersecting allows to move the object in front - * of the first intersecting object. Where intersection is calculated with - * bounding box. If no intersection is found, there will not be change in the - * stack. - * @param {fabric.Object} object Object to send - * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object - * @return {fabric.Canvas} thisArg - * @chainable - */ - bringForward: function (object, intersecting) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, obj, idx, newIdx, objs, objsMoved = 0; - - if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = objs.length; i--;) { - obj = objs[i]; - idx = this._objects.indexOf(obj); - if (idx < this._objects.length - 1 - objsMoved) { - newIdx = idx + 1; - removeFromArray(this._objects, obj); - this._objects.splice(newIdx, 0, obj); - } - objsMoved++; - } - } - else { - idx = this._objects.indexOf(object); - if (idx !== this._objects.length - 1) { - // if object is not on top of stack (last item in an array) - newIdx = this._findNewUpperIndex(object, idx, intersecting); - removeFromArray(this._objects, object); - this._objects.splice(newIdx, 0, object); - } - } - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * @private - */ - _findNewUpperIndex: function (object, idx, intersecting) { - var newIdx, i, len; - - if (intersecting) { - newIdx = idx; - - // traverse up the stack looking for the nearest intersecting object - for (i = idx + 1, len = this._objects.length; i < len; ++i) { - - var isIntersecting = object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } - else { - newIdx = idx + 1; - } - - return newIdx; - }, - - /** - * Moves an object to specified level in stack of drawn objects - * @param {fabric.Object} object Object to send - * @param {Number} index Position to move to - * @return {fabric.Canvas} thisArg - * @chainable - */ - moveTo: function (object, index) { - removeFromArray(this._objects, object); - this._objects.splice(index, 0, object); - return this.renderOnAddRemove && this.requestRenderAll(); - }, - - /** - * Clears a canvas element and dispose objects - * @return {fabric.Canvas} thisArg - * @chainable - */ - dispose: function () { - // cancel eventually ongoing renders - if (this.isRendering) { - fabric.util.cancelAnimFrame(this.isRendering); - this.isRendering = 0; - } - this.forEachObject(function (object) { - object.dispose && object.dispose(); - }); - this._objects = []; - if (this.backgroundImage && this.backgroundImage.dispose) { - this.backgroundImage.dispose(); - } - this.backgroundImage = null; - if (this.overlayImage && this.overlayImage.dispose) { - this.overlayImage.dispose(); - } - this.overlayImage = null; - this._iTextInstances = null; - this.contextContainer = null; - // restore canvas style - this.lowerCanvasEl.classList.remove('lower-canvas'); - fabric.util.setStyle(this.lowerCanvasEl, this._originalCanvasStyle); - delete this._originalCanvasStyle; - // restore canvas size to original size in case retina scaling was applied - this.lowerCanvasEl.setAttribute('width', this.width); - this.lowerCanvasEl.setAttribute('height', this.height); - fabric.util.cleanUpJsdomNode(this.lowerCanvasEl); - this.lowerCanvasEl = undefined; - return this; - }, - - /** - * Returns a string representation of an instance - * @return {String} string representation of an instance - */ - toString: function () { - return '#'; - } - }); - - extend(fabric.StaticCanvas.prototype, fabric.Observable); - extend(fabric.StaticCanvas.prototype, fabric.Collection); - extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); - - extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ { - - /** - * @static - * @type String - * @default - */ - EMPTY_JSON: '{"objects": [], "background": "white"}', - - /** - * Provides a way to check support of some of the canvas methods - * (either those of HTMLCanvasElement itself, or rendering context) - * - * @param {String} methodName Method to check support for; - * Could be one of "setLineDash" - * @return {Boolean | null} `true` if method is supported (or at least exists), - * `null` if canvas element or context can not be initialized - */ - supports: function (methodName) { - var el = createCanvasElement(); - - if (!el || !el.getContext) { - return null; - } - - var ctx = el.getContext('2d'); - if (!ctx) { - return null; - } - - switch (methodName) { - - case 'setLineDash': - return typeof ctx.setLineDash !== 'undefined'; - - default: - return null; - } - } - }); - - /** - * Returns Object representation of canvas - * this alias is provided because if you call JSON.stringify on an instance, - * the toJSON object will be invoked if it exists. - * Having a toJSON method means you can do JSON.stringify(myCanvas) - * @function - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} JSON compatible object - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} - * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo} - * @example JSON without additional properties - * var json = canvas.toJSON(); - * @example JSON with additional properties included - * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY']); - * @example JSON without default values - * canvas.includeDefaultValues = false; - * var json = canvas.toJSON(); - */ - fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; - - if (fabric.isLikelyNode) { - fabric.StaticCanvas.prototype.createPNGStream = function () { - var impl = getNodeCanvas(this.lowerCanvasEl); - return impl && impl.createPNGStream(); - }; - fabric.StaticCanvas.prototype.createJPEGStream = function (opts) { - var impl = getNodeCanvas(this.lowerCanvasEl); - return impl && impl.createJPEGStream(opts); - }; - } -})(); - - -/** - * BaseBrush class - * @class fabric.BaseBrush - * @see {@link http://fabricjs.com/freedrawing|Freedrawing demo} - */ -fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ { - - /** - * Color of a brush - * @type String - * @default - */ - color: 'rgb(0, 0, 0)', - - /** - * Width of a brush, has to be a Number, no string literals - * @type Number - * @default - */ - width: 1, - - /** - * Shadow object representing shadow of this shape. - * Backwards incompatibility note: This property replaces "shadowColor" (String), "shadowOffsetX" (Number), - * "shadowOffsetY" (Number) and "shadowBlur" (Number) since v1.2.12 - * @type fabric.Shadow - * @default - */ - shadow: null, - - /** - * Line endings style of a brush (one of "butt", "round", "square") - * @type String - * @default - */ - strokeLineCap: 'round', - - /** - * Corner style of a brush (one of "bevel", "round", "miter") - * @type String - * @default - */ - strokeLineJoin: 'round', - - /** - * Maximum miter length (used for strokeLineJoin = "miter") of a brush's - * @type Number - * @default - */ - strokeMiterLimit: 10, - - /** - * Stroke Dash Array. - * @type Array - * @default - */ - strokeDashArray: null, - - /** - * When `true`, the free drawing is limited to the whiteboard size. Default to false. - * @type Boolean - * @default false - */ - - limitedToCanvasSize: false, - - - /** - * Sets brush styles - * @private - * @param {CanvasRenderingContext2D} ctx - */ - _setBrushStyles: function (ctx) { - ctx.strokeStyle = this.color; - ctx.lineWidth = this.width; - ctx.lineCap = this.strokeLineCap; - ctx.miterLimit = this.strokeMiterLimit; - ctx.lineJoin = this.strokeLineJoin; - ctx.setLineDash(this.strokeDashArray || []); - }, - - /** - * Sets the transformation on given context - * @param {RenderingContext2d} ctx context to render on - * @private - */ - _saveAndTransform: function (ctx) { - var v = this.canvas.viewportTransform; - ctx.save(); - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - }, - - /** - * Sets brush shadow styles - * @private - */ - _setShadow: function () { - if (!this.shadow) { - return; - } - - var canvas = this.canvas, - shadow = this.shadow, - ctx = canvas.contextTop, - zoom = canvas.getZoom(); - if (canvas && canvas._isRetinaScaling()) { - zoom *= fabric.devicePixelRatio; - } - - ctx.shadowColor = shadow.color; - ctx.shadowBlur = shadow.blur * zoom; - ctx.shadowOffsetX = shadow.offsetX * zoom; - ctx.shadowOffsetY = shadow.offsetY * zoom; - }, - - needsFullRender: function () { - var color = new fabric.Color(this.color); - return color.getAlpha() < 1 || !!this.shadow; - }, - - /** - * Removes brush shadow styles - * @private - */ - _resetShadow: function () { - var ctx = this.canvas.contextTop; - - ctx.shadowColor = ''; - ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; - }, - - /** - * Check is pointer is outside canvas boundaries - * @param {Object} pointer - * @private - */ - _isOutSideCanvas: function (pointer) { - return pointer.x < 0 || pointer.x > this.canvas.getWidth() || pointer.y < 0 || pointer.y > this.canvas.getHeight(); - } -}); - - -(function () { - /** - * PencilBrush class - * @class fabric.PencilBrush - * @extends fabric.BaseBrush - */ - fabric.PencilBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.PencilBrush.prototype */ { - - /** - * Discard points that are less than `decimate` pixel distant from each other - * @type Number - * @default 0.4 - */ - decimate: 0.4, - - /** - * Draws a straight line between last recorded point to current pointer - * Used for `shift` functionality - * - * @type boolean - * @default false - */ - drawStraightLine: false, - - /** - * The event modifier key that makes the brush draw a straight line. - * If `null` or 'none' or any other string that is not a modifier key the feature is disabled. - * @type {'altKey' | 'shiftKey' | 'ctrlKey' | 'none' | undefined | null} - */ - straightLineKey: 'shiftKey', - - /** - * Constructor - * @param {fabric.Canvas} canvas - * @return {fabric.PencilBrush} Instance of a pencil brush - */ - initialize: function (canvas) { - this.canvas = canvas; - this._points = []; - }, - - needsFullRender: function () { - return this.callSuper('needsFullRender') || this._hasStraightLine; - }, - - /** - * Invoked inside on mouse down and mouse move - * @param {Object} pointer - */ - _drawSegment: function (ctx, p1, p2) { - var midPoint = p1.midPointFrom(p2); - ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y); - return midPoint; - }, - - /** - * Invoked on mouse down - * @param {Object} pointer - */ - onMouseDown: function (pointer, options) { - if (!this.canvas._isMainEvent(options.e)) { - return; - } - this.drawStraightLine = options.e[this.straightLineKey]; - this._prepareForDrawing(pointer); - // capture coordinates immediately - // this allows to draw dots (when movement never occurs) - this._captureDrawingPath(pointer); - this._render(); - }, - - /** - * Invoked on mouse move - * @param {Object} pointer - */ - onMouseMove: function (pointer, options) { - if (!this.canvas._isMainEvent(options.e)) { - return; - } - this.drawStraightLine = options.e[this.straightLineKey]; - if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { - return; - } - if (this._captureDrawingPath(pointer) && this._points.length > 1) { - if (this.needsFullRender()) { - // redraw curve - // clear top canvas - this.canvas.clearContext(this.canvas.contextTop); - this._render(); - } - else { - var points = this._points, length = points.length, ctx = this.canvas.contextTop; - // draw the curve update - this._saveAndTransform(ctx); - if (this.oldEnd) { - ctx.beginPath(); - ctx.moveTo(this.oldEnd.x, this.oldEnd.y); - } - this.oldEnd = this._drawSegment(ctx, points[length - 2], points[length - 1], true); - ctx.stroke(); - ctx.restore(); - } - } - }, - - /** - * Invoked on mouse up - */ - onMouseUp: function (options) { - if (!this.canvas._isMainEvent(options.e)) { - return true; - } - this.drawStraightLine = false; - this.oldEnd = undefined; - this._finalizeAndAddPath(); - return false; - }, - - /** - * @private - * @param {Object} pointer Actual mouse position related to the canvas. - */ - _prepareForDrawing: function (pointer) { - - var p = new fabric.Point(pointer.x, pointer.y); - - this._reset(); - this._addPoint(p); - this.canvas.contextTop.moveTo(p.x, p.y); - }, - - /** - * @private - * @param {fabric.Point} point Point to be added to points array - */ - _addPoint: function (point) { - if (this._points.length > 1 && point.eq(this._points[this._points.length - 1])) { - return false; - } - if (this.drawStraightLine && this._points.length > 1) { - this._hasStraightLine = true; - this._points.pop(); - } - this._points.push(point); - return true; - }, - - /** - * Clear points array and set contextTop canvas style. - * @private - */ - _reset: function () { - this._points = []; - this._setBrushStyles(this.canvas.contextTop); - this._setShadow(); - this._hasStraightLine = false; - }, - - /** - * @private - * @param {Object} pointer Actual mouse position related to the canvas. - */ - _captureDrawingPath: function (pointer) { - var pointerPoint = new fabric.Point(pointer.x, pointer.y); - return this._addPoint(pointerPoint); - }, - - /** - * Draw a smooth path on the topCanvas using quadraticCurveTo - * @private - * @param {CanvasRenderingContext2D} [ctx] - */ - _render: function (ctx) { - var i, len, - p1 = this._points[0], - p2 = this._points[1]; - ctx = ctx || this.canvas.contextTop; - this._saveAndTransform(ctx); - ctx.beginPath(); - //if we only have 2 points in the path and they are the same - //it means that the user only clicked the canvas without moving the mouse - //then we should be drawing a dot. A path isn't drawn between two identical dots - //that's why we set them apart a bit - if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) { - var width = this.width / 1000; - p1 = new fabric.Point(p1.x, p1.y); - p2 = new fabric.Point(p2.x, p2.y); - p1.x -= width; - p2.x += width; - } - ctx.moveTo(p1.x, p1.y); - - for (i = 1, len = this._points.length; i < len; i++) { - // we pick the point between pi + 1 & pi + 2 as the - // end point and p1 as our control point. - this._drawSegment(ctx, p1, p2); - p1 = this._points[i]; - p2 = this._points[i + 1]; - } - // Draw last line as a straight line while - // we wait for the next point to be able to calculate - // the bezier control point - ctx.lineTo(p1.x, p1.y); - ctx.stroke(); - ctx.restore(); - }, - - /** - * Converts points to SVG path - * @param {Array} points Array of points - * @return {(string|number)[][]} SVG path commands - */ - convertPointsToSVGPath: function (points) { - var correction = this.width / 1000; - return fabric.util.getSmoothPathFromPoints(points, correction); - }, - - /** - * @private - * @param {(string|number)[][]} pathData SVG path commands - * @returns {boolean} - */ - _isEmptySVGPath: function (pathData) { - var pathString = fabric.util.joinPath(pathData); - return pathString === 'M 0 0 Q 0 0 0 0 L 0 0'; - }, - - /** - * Creates fabric.Path object to add on canvas - * @param {(string|number)[][]} pathData Path data - * @return {fabric.Path} Path to add on canvas - */ - createPath: function (pathData) { - var path = new fabric.Path(pathData, { - fill: null, - stroke: this.color, - strokeWidth: this.width, - strokeLineCap: this.strokeLineCap, - strokeMiterLimit: this.strokeMiterLimit, - strokeLineJoin: this.strokeLineJoin, - strokeDashArray: this.strokeDashArray, - }); - if (this.shadow) { - this.shadow.affectStroke = true; - path.shadow = new fabric.Shadow(this.shadow); - } - - return path; - }, - - /** - * Decimate points array with the decimate value - */ - decimatePoints: function (points, distance) { - if (points.length <= 2) { - return points; - } - var zoom = this.canvas.getZoom(), adjustedDistance = Math.pow(distance / zoom, 2), - i, l = points.length - 1, lastPoint = points[0], newPoints = [lastPoint], - cDistance; - for (i = 1; i < l - 1; i++) { - cDistance = Math.pow(lastPoint.x - points[i].x, 2) + Math.pow(lastPoint.y - points[i].y, 2); - if (cDistance >= adjustedDistance) { - lastPoint = points[i]; - newPoints.push(lastPoint); - } - } - /** - * Add the last point from the original line to the end of the array. - * This ensures decimate doesn't delete the last point on the line, and ensures the line is > 1 point. - */ - newPoints.push(points[l]); - return newPoints; - }, - - /** - * On mouseup after drawing the path on contextTop canvas - * we use the points captured to create an new fabric path object - * and add it to the fabric canvas. - */ - _finalizeAndAddPath: function () { - var ctx = this.canvas.contextTop; - ctx.closePath(); - if (this.decimate) { - this._points = this.decimatePoints(this._points, this.decimate); - } - var pathData = this.convertPointsToSVGPath(this._points); - if (this._isEmptySVGPath(pathData)) { - // do not create 0 width/height paths, as they are - // rendered inconsistently across browsers - // Firefox 4, for example, renders a dot, - // whereas Chrome 10 renders nothing - this.canvas.requestRenderAll(); - return; - } - - var path = this.createPath(pathData); - this.canvas.clearContext(this.canvas.contextTop); - this.canvas.fire('before:path:created', { path: path }); - this.canvas.add(path); - this.canvas.requestRenderAll(); - path.setCoords(); - this._resetShadow(); - - - // fire event 'path' created - this.canvas.fire('path:created', { path: path }); - } - }); -})(); - - -/** - * CircleBrush class - * @class fabric.CircleBrush - */ -fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.CircleBrush.prototype */ { - - /** - * Width of a brush - * @type Number - * @default - */ - width: 10, - - /** - * Constructor - * @param {fabric.Canvas} canvas - * @return {fabric.CircleBrush} Instance of a circle brush - */ - initialize: function (canvas) { - this.canvas = canvas; - this.points = []; - }, - - /** - * Invoked inside on mouse down and mouse move - * @param {Object} pointer - */ - drawDot: function (pointer) { - var point = this.addPoint(pointer), - ctx = this.canvas.contextTop; - this._saveAndTransform(ctx); - this.dot(ctx, point); - ctx.restore(); - }, - - dot: function (ctx, point) { - ctx.fillStyle = point.fill; - ctx.beginPath(); - ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); - ctx.closePath(); - ctx.fill(); - }, - - /** - * Invoked on mouse down - */ - onMouseDown: function (pointer) { - this.points.length = 0; - this.canvas.clearContext(this.canvas.contextTop); - this._setShadow(); - this.drawDot(pointer); - }, - - /** - * Render the full state of the brush - * @private - */ - _render: function () { - var ctx = this.canvas.contextTop, i, len, - points = this.points; - this._saveAndTransform(ctx); - for (i = 0, len = points.length; i < len; i++) { - this.dot(ctx, points[i]); - } - ctx.restore(); - }, - - /** - * Invoked on mouse move - * @param {Object} pointer - */ - onMouseMove: function (pointer) { - if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { - return; - } - if (this.needsFullRender()) { - this.canvas.clearContext(this.canvas.contextTop); - this.addPoint(pointer); - this._render(); - } - else { - this.drawDot(pointer); - } - }, - - /** - * Invoked on mouse up - */ - onMouseUp: function () { - var originalRenderOnAddRemove = this.canvas.renderOnAddRemove, i, len; - this.canvas.renderOnAddRemove = false; - - var circles = []; - - for (i = 0, len = this.points.length; i < len; i++) { - var point = this.points[i], - circle = new fabric.Circle({ - radius: point.radius, - left: point.x, - top: point.y, - originX: 'center', - originY: 'center', - fill: point.fill - }); - - this.shadow && (circle.shadow = new fabric.Shadow(this.shadow)); - - circles.push(circle); - } - var group = new fabric.Group(circles); - group.canvas = this.canvas; - - this.canvas.fire('before:path:created', { path: group }); - this.canvas.add(group); - this.canvas.fire('path:created', { path: group }); - - this.canvas.clearContext(this.canvas.contextTop); - this._resetShadow(); - this.canvas.renderOnAddRemove = originalRenderOnAddRemove; - this.canvas.requestRenderAll(); - }, - - /** - * @param {Object} pointer - * @return {fabric.Point} Just added pointer point - */ - addPoint: function (pointer) { - var pointerPoint = new fabric.Point(pointer.x, pointer.y), - - circleRadius = fabric.util.getRandomInt( - Math.max(0, this.width - 20), this.width + 20) / 2, - - circleColor = new fabric.Color(this.color) - .setAlpha(fabric.util.getRandomInt(0, 100) / 100) - .toRgba(); - - pointerPoint.radius = circleRadius; - pointerPoint.fill = circleColor; - - this.points.push(pointerPoint); - - return pointerPoint; - } -}); - - -/** - * SprayBrush class - * @class fabric.SprayBrush - */ -fabric.SprayBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.SprayBrush.prototype */ { - - /** - * Width of a spray - * @type Number - * @default - */ - width: 10, - - /** - * Density of a spray (number of dots per chunk) - * @type Number - * @default - */ - density: 20, - - /** - * Width of spray dots - * @type Number - * @default - */ - dotWidth: 1, - - /** - * Width variance of spray dots - * @type Number - * @default - */ - dotWidthVariance: 1, - - /** - * Whether opacity of a dot should be random - * @type Boolean - * @default - */ - randomOpacity: false, - - /** - * Whether overlapping dots (rectangles) should be removed (for performance reasons) - * @type Boolean - * @default - */ - optimizeOverlapping: true, - - /** - * Constructor - * @param {fabric.Canvas} canvas - * @return {fabric.SprayBrush} Instance of a spray brush - */ - initialize: function (canvas) { - this.canvas = canvas; - this.sprayChunks = []; - }, - - /** - * Invoked on mouse down - * @param {Object} pointer - */ - onMouseDown: function (pointer) { - this.sprayChunks.length = 0; - this.canvas.clearContext(this.canvas.contextTop); - this._setShadow(); - - this.addSprayChunk(pointer); - this.render(this.sprayChunkPoints); - }, - - /** - * Invoked on mouse move - * @param {Object} pointer - */ - onMouseMove: function (pointer) { - if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { - return; - } - this.addSprayChunk(pointer); - this.render(this.sprayChunkPoints); - }, - - /** - * Invoked on mouse up - */ - onMouseUp: function () { - var originalRenderOnAddRemove = this.canvas.renderOnAddRemove; - this.canvas.renderOnAddRemove = false; - - var rects = []; - - for (var i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { - var sprayChunk = this.sprayChunks[i]; - - for (var j = 0, jlen = sprayChunk.length; j < jlen; j++) { - - var rect = new fabric.Rect({ - width: sprayChunk[j].width, - height: sprayChunk[j].width, - left: sprayChunk[j].x + 1, - top: sprayChunk[j].y + 1, - originX: 'center', - originY: 'center', - fill: this.color - }); - rects.push(rect); - } - } - - if (this.optimizeOverlapping) { - rects = this._getOptimizedRects(rects); - } - - var group = new fabric.Group(rects); - this.shadow && group.set('shadow', new fabric.Shadow(this.shadow)); - this.canvas.fire('before:path:created', { path: group }); - this.canvas.add(group); - this.canvas.fire('path:created', { path: group }); - - this.canvas.clearContext(this.canvas.contextTop); - this._resetShadow(); - this.canvas.renderOnAddRemove = originalRenderOnAddRemove; - this.canvas.requestRenderAll(); - }, - - /** - * @private - * @param {Array} rects - */ - _getOptimizedRects: function (rects) { - - // avoid creating duplicate rects at the same coordinates - var uniqueRects = {}, key, i, len; - - for (i = 0, len = rects.length; i < len; i++) { - key = rects[i].left + '' + rects[i].top; - if (!uniqueRects[key]) { - uniqueRects[key] = rects[i]; - } - } - var uniqueRectsArray = []; - for (key in uniqueRects) { - uniqueRectsArray.push(uniqueRects[key]); - } - - return uniqueRectsArray; - }, - - /** - * Render new chunk of spray brush - */ - render: function (sprayChunk) { - var ctx = this.canvas.contextTop, i, len; - ctx.fillStyle = this.color; - - this._saveAndTransform(ctx); - - for (i = 0, len = sprayChunk.length; i < len; i++) { - var point = sprayChunk[i]; - if (typeof point.opacity !== 'undefined') { - ctx.globalAlpha = point.opacity; - } - ctx.fillRect(point.x, point.y, point.width, point.width); - } - ctx.restore(); - }, - - /** - * Render all spray chunks - */ - _render: function () { - var ctx = this.canvas.contextTop, i, ilen; - ctx.fillStyle = this.color; - - this._saveAndTransform(ctx); - - for (i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { - this.render(this.sprayChunks[i]); - } - ctx.restore(); - }, - - /** - * @param {Object} pointer - */ - addSprayChunk: function (pointer) { - this.sprayChunkPoints = []; - - var x, y, width, radius = this.width / 2, i; - - for (i = 0; i < this.density; i++) { - - x = fabric.util.getRandomInt(pointer.x - radius, pointer.x + radius); - y = fabric.util.getRandomInt(pointer.y - radius, pointer.y + radius); - - if (this.dotWidthVariance) { - width = fabric.util.getRandomInt( - // bottom clamp width to 1 - Math.max(1, this.dotWidth - this.dotWidthVariance), - this.dotWidth + this.dotWidthVariance); - } - else { - width = this.dotWidth; - } - - var point = new fabric.Point(x, y); - point.width = width; - - if (this.randomOpacity) { - point.opacity = fabric.util.getRandomInt(0, 100) / 100; - } - - this.sprayChunkPoints.push(point); - } - - this.sprayChunks.push(this.sprayChunkPoints); - } -}); - - -/** - * PatternBrush class - * @class fabric.PatternBrush - * @extends fabric.BaseBrush - */ -fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fabric.PatternBrush.prototype */ { - - getPatternSrc: function () { - - var dotWidth = 20, - dotDistance = 5, - patternCanvas = fabric.util.createCanvasElement(), - patternCtx = patternCanvas.getContext('2d'); - - patternCanvas.width = patternCanvas.height = dotWidth + dotDistance; - - patternCtx.fillStyle = this.color; - patternCtx.beginPath(); - patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false); - patternCtx.closePath(); - patternCtx.fill(); - - return patternCanvas; - }, - - getPatternSrcFunction: function () { - return String(this.getPatternSrc).replace('this.color', '"' + this.color + '"'); - }, - - /** - * Creates "pattern" instance property - * @param {CanvasRenderingContext2D} ctx - */ - getPattern: function (ctx) { - return ctx.createPattern(this.source || this.getPatternSrc(), 'repeat'); - }, - - /** - * Sets brush styles - * @param {CanvasRenderingContext2D} ctx - */ - _setBrushStyles: function (ctx) { - this.callSuper('_setBrushStyles', ctx); - ctx.strokeStyle = this.getPattern(ctx); - }, - - /** - * Creates path - */ - createPath: function (pathData) { - var path = this.callSuper('createPath', pathData), - topLeft = path._getLeftTopCoords().scalarAdd(path.strokeWidth / 2); - - path.stroke = new fabric.Pattern({ - source: this.source || this.getPatternSrcFunction(), - offsetX: -topLeft.x, - offsetY: -topLeft.y - }); - return path; - } -}); - - -(function () { - - var getPointer = fabric.util.getPointer, - degreesToRadians = fabric.util.degreesToRadians, - isTouchEvent = fabric.util.isTouchEvent; - - /** - * Canvas class - * @class fabric.Canvas - * @extends fabric.StaticCanvas - * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#canvas} - * @see {@link fabric.Canvas#initialize} for constructor definition - * - * @fires object:modified at the end of a transform or any change when statefull is true - * @fires object:rotating while an object is being rotated from the control - * @fires object:scaling while an object is being scaled by controls - * @fires object:moving while an object is being dragged - * @fires object:skewing while an object is being skewed from the controls - * - * @fires before:transform before a transform is is started - * @fires before:selection:cleared - * @fires selection:cleared - * @fires selection:updated - * @fires selection:created - * - * @fires path:created after a drawing operation ends and the path is added - * @fires mouse:down - * @fires mouse:move - * @fires mouse:up - * @fires mouse:down:before on mouse down, before the inner fabric logic runs - * @fires mouse:move:before on mouse move, before the inner fabric logic runs - * @fires mouse:up:before on mouse up, before the inner fabric logic runs - * @fires mouse:over - * @fires mouse:out - * @fires mouse:dblclick whenever a native dbl click event fires on the canvas. - * - * @fires dragover - * @fires dragenter - * @fires dragleave - * @fires drop:before before drop event. same native event. This is added to handle edge cases - * @fires drop - * @fires after:render at the end of the render process, receives the context in the callback - * @fires before:render at start the render process, receives the context in the callback - * - */ - fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { - - /** - * Constructor - * @param {HTMLElement | String} el <canvas> element to initialize instance on - * @param {Object} [options] Options object - * @return {Object} thisArg - */ - initialize: function (el, options) { - options || (options = {}); - this.renderAndResetBound = this.renderAndReset.bind(this); - this.requestRenderAllBound = this.requestRenderAll.bind(this); - this._initStatic(el, options); - this._initInteractive(); - this._createCacheCanvas(); - }, - - /** - * When true, objects can be transformed by one side (unproportionally) - * when dragged on the corners that normally would not do that. - * @type Boolean - * @default - * @since fabric 4.0 // changed name and default value - */ - uniformScaling: true, - - /** - * Indicates which key switches uniform scaling. - * values: 'altKey', 'shiftKey', 'ctrlKey'. - * If `null` or 'none' or any other string that is not a modifier key - * feature is disabled. - * totally wrong named. this sounds like `uniform scaling` - * if Canvas.uniformScaling is true, pressing this will set it to false - * and viceversa. - * @since 1.6.2 - * @type String - * @default - */ - uniScaleKey: 'shiftKey', - - /** - * When true, objects use center point as the origin of scale transformation. - * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). - * @since 1.3.4 - * @type Boolean - * @default - */ - centeredScaling: false, - - /** - * When true, objects use center point as the origin of rotate transformation. - * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). - * @since 1.3.4 - * @type Boolean - * @default - */ - centeredRotation: false, - - /** - * Indicates which key enable centered Transform - * values: 'altKey', 'shiftKey', 'ctrlKey'. - * If `null` or 'none' or any other string that is not a modifier key - * feature is disabled feature disabled. - * @since 1.6.2 - * @type String - * @default - */ - centeredKey: 'altKey', - - /** - * Indicates which key enable alternate action on corner - * values: 'altKey', 'shiftKey', 'ctrlKey'. - * If `null` or 'none' or any other string that is not a modifier key - * feature is disabled feature disabled. - * @since 1.6.2 - * @type String - * @default - */ - altActionKey: 'shiftKey', - - /** - * Indicates that canvas is interactive. This property should not be changed. - * @type Boolean - * @default - */ - interactive: true, - - /** - * Indicates whether group selection should be enabled - * @type Boolean - * @default - */ - selection: true, - - /** - * Indicates which key or keys enable multiple click selection - * Pass value as a string or array of strings - * values: 'altKey', 'shiftKey', 'ctrlKey'. - * If `null` or empty or containing any other string that is not a modifier key - * feature is disabled. - * @since 1.6.2 - * @type String|Array - * @default - */ - selectionKey: 'shiftKey', - - /** - * Indicates which key enable alternative selection - * in case of target overlapping with active object - * values: 'altKey', 'shiftKey', 'ctrlKey'. - * For a series of reason that come from the general expectations on how - * things should work, this feature works only for preserveObjectStacking true. - * If `null` or 'none' or any other string that is not a modifier key - * feature is disabled. - * @since 1.6.5 - * @type null|String - * @default - */ - altSelectionKey: null, - - /** - * Color of selection - * @type String - * @default - */ - selectionColor: 'rgba(100, 100, 255, 0.3)', // blue - - /** - * Default dash array pattern - * If not empty the selection border is dashed - * @type Array - */ - selectionDashArray: [], - - /** - * Color of the border of selection (usually slightly darker than color of selection itself) - * @type String - * @default - */ - selectionBorderColor: 'rgba(255, 255, 255, 0.3)', - - /** - * Width of a line used in object/group selection - * @type Number - * @default - */ - selectionLineWidth: 1, - - /** - * Select only shapes that are fully contained in the dragged selection rectangle. - * @type Boolean - * @default - */ - selectionFullyContained: false, - - /** - * Default cursor value used when hovering over an object on canvas - * @type String - * @default - */ - hoverCursor: 'move', - - /** - * Default cursor value used when moving an object on canvas - * @type String - * @default - */ - moveCursor: 'move', - - /** - * Default cursor value used for the entire canvas - * @type String - * @default - */ - defaultCursor: 'default', - - /** - * Cursor value used during free drawing - * @type String - * @default - */ - freeDrawingCursor: 'crosshair', - - /** - * Cursor value used for disabled elements ( corners with disabled action ) - * @type String - * @since 2.0.0 - * @default - */ - notAllowedCursor: 'not-allowed', - - /** - * Default element class that's given to wrapper (div) element of canvas - * @type String - * @default - */ - containerClass: 'canvas-container', - - /** - * When true, object detection happens on per-pixel basis rather than on per-bounding-box - * @type Boolean - * @default - */ - perPixelTargetFind: false, - - /** - * Number of pixels around target pixel to tolerate (consider active) during object detection - * @type Number - * @default - */ - targetFindTolerance: 0, - - /** - * When true, target detection is skipped. Target detection will return always undefined. - * click selection won't work anymore, events will fire with no targets. - * if something is selected before setting it to true, it will be deselected at the first click. - * area selection will still work. check the `selection` property too. - * if you deactivate both, you should look into staticCanvas. - * @type Boolean - * @default - */ - skipTargetFind: false, - - /** - * When true, mouse events on canvas (mousedown/mousemove/mouseup) result in free drawing. - * After mousedown, mousemove creates a shape, - * and then mouseup finalizes it and adds an instance of `fabric.Path` onto canvas. - * @tutorial {@link http://fabricjs.com/fabric-intro-part-4#free_drawing} - * @type Boolean - * @default - */ - isDrawingMode: false, - - /** - * Indicates whether objects should remain in current stack position when selected. - * When false objects are brought to top and rendered as part of the selection group - * @type Boolean - * @default - */ - preserveObjectStacking: false, - - /** - * Indicates the angle that an object will lock to while rotating. - * @type Number - * @since 1.6.7 - * @default - */ - snapAngle: 0, - - /** - * Indicates the distance from the snapAngle the rotation will lock to the snapAngle. - * When `null`, the snapThreshold will default to the snapAngle. - * @type null|Number - * @since 1.6.7 - * @default - */ - snapThreshold: null, - - /** - * Indicates if the right click on canvas can output the context menu or not - * @type Boolean - * @since 1.6.5 - * @default - */ - stopContextMenu: false, - - /** - * Indicates if the canvas can fire right click events - * @type Boolean - * @since 1.6.5 - * @default - */ - fireRightClick: false, - - /** - * Indicates if the canvas can fire middle click events - * @type Boolean - * @since 1.7.8 - * @default - */ - fireMiddleClick: false, - - /** - * Keep track of the subTargets for Mouse Events - * @type fabric.Object[] - */ - targets: [], - - /** - * When the option is enabled, PointerEvent is used instead of MouseEvent. - * @type Boolean - * @default - */ - enablePointerEvents: false, - - /** - * Keep track of the hovered target - * @type fabric.Object - * @private - */ - _hoveredTarget: null, - - /** - * hold the list of nested targets hovered - * @type fabric.Object[] - * @private - */ - _hoveredTargets: [], - - /** - * @private - */ - _initInteractive: function () { - this._currentTransform = null; - this._groupSelector = null; - this._initWrapperElement(); - this._createUpperCanvas(); - this._initEventListeners(); - - this._initRetinaScaling(); - - this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); - - this.calcOffset(); - }, - - /** - * Divides objects in two groups, one to render immediately - * and one to render as activeGroup. - * @return {Array} objects to render immediately and pushes the other in the activeGroup. - */ - _chooseObjectsToRender: function () { - var activeObjects = this.getActiveObjects(), - object, objsToRender, activeGroupObjects; - - if (activeObjects.length > 0 && !this.preserveObjectStacking) { - objsToRender = []; - activeGroupObjects = []; - for (var i = 0, length = this._objects.length; i < length; i++) { - object = this._objects[i]; - if (activeObjects.indexOf(object) === -1) { - objsToRender.push(object); - } - else { - activeGroupObjects.push(object); - } - } - if (activeObjects.length > 1) { - this._activeObject._objects = activeGroupObjects; - } - objsToRender.push.apply(objsToRender, activeGroupObjects); - } - else { - objsToRender = this._objects; - } - return objsToRender; - }, - - /** - * Renders both the top canvas and the secondary container canvas. - * @return {fabric.Canvas} instance - * @chainable - */ - renderAll: function () { - if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) { - this.clearContext(this.contextTop); - this.contextTopDirty = false; - } - if (this.hasLostContext) { - this.renderTopLayer(this.contextTop); - this.hasLostContext = false; - } - var canvasToDrawOn = this.contextContainer; - this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); - return this; - }, - - renderTopLayer: function (ctx) { - ctx.save(); - if (this.isDrawingMode && this._isCurrentlyDrawing) { - this.freeDrawingBrush && this.freeDrawingBrush._render(); - this.contextTopDirty = true; - } - // we render the top context - last object - if (this.selection && this._groupSelector) { - this._drawSelection(ctx); - this.contextTopDirty = true; - } - ctx.restore(); - }, - - /** - * Method to render only the top canvas. - * Also used to render the group selection box. - * @return {fabric.Canvas} thisArg - * @chainable - */ - renderTop: function () { - var ctx = this.contextTop; - this.clearContext(ctx); - this.renderTopLayer(ctx); - this.fire('after:render'); - return this; - }, - - /** - * @private - */ - _normalizePointer: function (object, pointer) { - var m = object.calcTransformMatrix(), - invertedM = fabric.util.invertTransform(m), - vptPointer = this.restorePointerVpt(pointer); - return fabric.util.transformPoint(vptPointer, invertedM); - }, - - /** - * Returns true if object is transparent at a certain location - * @param {fabric.Object} target Object to check - * @param {Number} x Left coordinate - * @param {Number} y Top coordinate - * @return {Boolean} - */ - isTargetTransparent: function (target, x, y) { - // in case the target is the activeObject, we cannot execute this optimization - // because we need to draw controls too. - if (target.shouldCache() && target._cacheCanvas && target !== this._activeObject) { - var normalizedPointer = this._normalizePointer(target, { x: x, y: y }), - targetRelativeX = Math.max(target.cacheTranslationX + (normalizedPointer.x * target.zoomX), 0), - targetRelativeY = Math.max(target.cacheTranslationY + (normalizedPointer.y * target.zoomY), 0); - - var isTransparent = fabric.util.isTransparent( - target._cacheContext, Math.round(targetRelativeX), Math.round(targetRelativeY), this.targetFindTolerance); - - return isTransparent; - } - - var ctx = this.contextCache, - originalColor = target.selectionBackgroundColor, v = this.viewportTransform; - - target.selectionBackgroundColor = ''; - - this.clearContext(ctx); - - ctx.save(); - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - target.render(ctx); - ctx.restore(); - - target.selectionBackgroundColor = originalColor; - - var isTransparent = fabric.util.isTransparent( - ctx, x, y, this.targetFindTolerance); - - return isTransparent; - }, - - /** - * takes an event and determines if selection key has been pressed - * @private - * @param {Event} e Event object - */ - _isSelectionKeyPressed: function (e) { - var selectionKeyPressed = false; - - if (Array.isArray(this.selectionKey)) { - selectionKeyPressed = !!this.selectionKey.find(function (key) { return e[key] === true; }); - } - else { - selectionKeyPressed = e[this.selectionKey]; - } - - return selectionKeyPressed; - }, - - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - */ - _shouldClearSelection: function (e, target) { - var activeObjects = this.getActiveObjects(), - activeObject = this._activeObject; - - return ( - !target - || - (target && - activeObject && - activeObjects.length > 1 && - activeObjects.indexOf(target) === -1 && - activeObject !== target && - !this._isSelectionKeyPressed(e)) - || - (target && !target.evented) - || - (target && - !target.selectable && - activeObject && - activeObject !== target) - ); - }, - - /** - * centeredScaling from object can't override centeredScaling from canvas. - * this should be fixed, since object setting should take precedence over canvas. - * also this should be something that will be migrated in the control properties. - * as ability to define the origin of the transformation that the control provide. - * @private - * @param {fabric.Object} target - * @param {String} action - * @param {Boolean} altKey - */ - _shouldCenterTransform: function (target, action, altKey) { - if (!target) { - return; - } - - var centerTransform; - - if (action === 'scale' || action === 'scaleX' || action === 'scaleY' || action === 'resizing') { - centerTransform = this.centeredScaling || target.centeredScaling; - } - else if (action === 'rotate') { - centerTransform = this.centeredRotation || target.centeredRotation; - } - - return centerTransform ? !altKey : altKey; - }, - - /** - * should disappear before release 4.0 - * @private - */ - _getOriginFromCorner: function (target, corner) { - var origin = { - x: target.originX, - y: target.originY - }; - - if (corner === 'ml' || corner === 'tl' || corner === 'bl') { - origin.x = 'right'; - } - else if (corner === 'mr' || corner === 'tr' || corner === 'br') { - origin.x = 'left'; - } - - if (corner === 'tl' || corner === 'mt' || corner === 'tr') { - origin.y = 'bottom'; - } - else if (corner === 'bl' || corner === 'mb' || corner === 'br') { - origin.y = 'top'; - } - return origin; - }, - - /** - * @private - * @param {Boolean} alreadySelected true if target is already selected - * @param {String} corner a string representing the corner ml, mr, tl ... - * @param {Event} e Event object - * @param {fabric.Object} [target] inserted back to help overriding. Unused - */ - _getActionFromCorner: function (alreadySelected, corner, e, target) { - if (!corner || !alreadySelected) { - return 'drag'; - } - var control = target.controls[corner]; - return control.getActionName(e, control, target); - }, - - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - */ - _setupCurrentTransform: function (e, target, alreadySelected) { - if (!target) { - return; - } - - var pointer = this.getPointer(e), corner = target.__corner, - control = target.controls[corner], - actionHandler = (alreadySelected && corner) ? - control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler, - action = this._getActionFromCorner(alreadySelected, corner, e, target), - origin = this._getOriginFromCorner(target, corner), - altKey = e[this.centeredKey], - transform = { - target: target, - action: action, - actionHandler: actionHandler, - corner: corner, - scaleX: target.scaleX, - scaleY: target.scaleY, - skewX: target.skewX, - skewY: target.skewY, - // used by transation - offsetX: pointer.x - target.left, - offsetY: pointer.y - target.top, - originX: origin.x, - originY: origin.y, - ex: pointer.x, - ey: pointer.y, - lastX: pointer.x, - lastY: pointer.y, - // unsure they are useful anymore. - // left: target.left, - // top: target.top, - theta: degreesToRadians(target.angle), - // end of unsure - width: target.width * target.scaleX, - shiftKey: e.shiftKey, - altKey: altKey, - original: fabric.util.saveObjectTransform(target), - }; - - if (this._shouldCenterTransform(target, action, altKey)) { - transform.originX = 'center'; - transform.originY = 'center'; - } - transform.original.originX = origin.x; - transform.original.originY = origin.y; - this._currentTransform = transform; - this._beforeTransform(e); - }, - - /** - * Set the cursor type of the canvas element - * @param {String} value Cursor type of the canvas element. - * @see http://www.w3.org/TR/css3-ui/#cursor - */ - setCursor: function (value) { - this.upperCanvasEl.style.cursor = value; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx to draw the selection on - */ - _drawSelection: function (ctx) { - var selector = this._groupSelector, - viewportStart = new fabric.Point(selector.ex, selector.ey), - start = fabric.util.transformPoint(viewportStart, this.viewportTransform), - viewportExtent = new fabric.Point(selector.ex + selector.left, selector.ey + selector.top), - extent = fabric.util.transformPoint(viewportExtent, this.viewportTransform), - minX = Math.min(start.x, extent.x), - minY = Math.min(start.y, extent.y), - maxX = Math.max(start.x, extent.x), - maxY = Math.max(start.y, extent.y), - strokeOffset = this.selectionLineWidth / 2; - - if (this.selectionColor) { - ctx.fillStyle = this.selectionColor; - ctx.fillRect(minX, minY, maxX - minX, maxY - minY); - } - - if (!this.selectionLineWidth || !this.selectionBorderColor) { - return; - } - ctx.lineWidth = this.selectionLineWidth; - ctx.strokeStyle = this.selectionBorderColor; - - minX += strokeOffset; - minY += strokeOffset; - maxX -= strokeOffset; - maxY -= strokeOffset; - // selection border - fabric.Object.prototype._setLineDash.call(this, ctx, this.selectionDashArray); - ctx.strokeRect(minX, minY, maxX - minX, maxY - minY); - }, - - /** - * Method that determines what object we are clicking on - * the skipGroup parameter is for internal use, is needed for shift+click action - * 11/09/2018 TODO: would be cool if findTarget could discern between being a full target - * or the outside part of the corner. - * @param {Event} e mouse event - * @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through - * @return {fabric.Object} the target found - */ - findTarget: function (e, skipGroup) { - if (this.skipTargetFind) { - return; - } - - var ignoreZoom = true, - pointer = this.getPointer(e, ignoreZoom), - activeObject = this._activeObject, - aObjects = this.getActiveObjects(), - activeTarget, activeTargetSubs, - isTouch = isTouchEvent(e), - shouldLookForActive = (aObjects.length > 1 && !skipGroup) || aObjects.length === 1; - - // first check current group (if one exists) - // active group does not check sub targets like normal groups. - // if active group just exits. - this.targets = []; - - // if we hit the corner of an activeObject, let's return that. - if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) { - return activeObject; - } - if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { - return activeObject; - } - if (aObjects.length === 1 && - activeObject === this._searchPossibleTargets([activeObject], pointer)) { - if (!this.preserveObjectStacking) { - return activeObject; - } - else { - activeTarget = activeObject; - activeTargetSubs = this.targets; - this.targets = []; - } - } - var target = this._searchPossibleTargets(this._objects, pointer); - if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { - target = activeTarget; - this.targets = activeTargetSubs; - } - return target; - }, - - /** - * Checks point is inside the object. - * @param {Object} [pointer] x,y object of point coordinates we want to check. - * @param {fabric.Object} obj Object to test against - * @param {Object} [globalPointer] x,y object of point coordinates relative to canvas used to search per pixel target. - * @return {Boolean} true if point is contained within an area of given object - * @private - */ - _checkTarget: function (pointer, obj, globalPointer) { - if (obj && - obj.visible && - obj.evented && - // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html - // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html - obj.containsPoint(pointer) - ) { - if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { - var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y); - if (!isTransparent) { - return true; - } - } - else { - return true; - } - } - }, - - /** - * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted - * @param {Array} [objects] objects array to look into - * @param {Object} [pointer] x,y object of point coordinates we want to check. - * @return {fabric.Object} object that contains pointer - * @private - */ - _searchPossibleTargets: function (objects, pointer) { - // Cache all targets where their bounding box contains point. - var target, i = objects.length, subTarget; - // Do not check for currently grouped objects, since we check the parent group itself. - // until we call this function specifically to search inside the activeGroup - while (i--) { - var objToCheck = objects[i]; - var pointerToUse = objToCheck.group ? - this._normalizePointer(objToCheck.group, pointer) : pointer; - if (this._checkTarget(pointerToUse, objToCheck, pointer)) { - target = objects[i]; - if (target.subTargetCheck && target instanceof fabric.Group) { - subTarget = this._searchPossibleTargets(target._objects, pointer); - subTarget && this.targets.push(subTarget); - } - break; - } - } - return target; - }, - - /** - * Returns pointer coordinates without the effect of the viewport - * @param {Object} pointer with "x" and "y" number values - * @return {Object} object with "x" and "y" number values - */ - restorePointerVpt: function (pointer) { - return fabric.util.transformPoint( - pointer, - fabric.util.invertTransform(this.viewportTransform) - ); - }, - - /** - * Returns pointer coordinates relative to canvas. - * Can return coordinates with or without viewportTransform. - * ignoreZoom false gives back coordinates that represent - * the point clicked on canvas element. - * ignoreZoom true gives back coordinates after being processed - * by the viewportTransform ( sort of coordinates of what is displayed - * on the canvas where you are clicking. - * ignoreZoom true = HTMLElement coordinates relative to top,left - * ignoreZoom false, default = fabric space coordinates, the same used for shape position - * To interact with your shapes top and left you want to use ignoreZoom true - * most of the time, while ignoreZoom false will give you coordinates - * compatible with the object.oCoords system. - * of the time. - * @param {Event} e - * @param {Boolean} ignoreZoom - * @return {Object} object with "x" and "y" number values - */ - getPointer: function (e, ignoreZoom) { - // return cached values if we are in the event processing chain - if (this._absolutePointer && !ignoreZoom) { - return this._absolutePointer; - } - if (this._pointer && ignoreZoom) { - return this._pointer; - } - - var pointer = getPointer(e), - upperCanvasEl = this.upperCanvasEl, - bounds = upperCanvasEl.getBoundingClientRect(), - boundsWidth = bounds.width || 0, - boundsHeight = bounds.height || 0, - cssScale; - - if (!boundsWidth || !boundsHeight) { - if ('top' in bounds && 'bottom' in bounds) { - boundsHeight = Math.abs(bounds.top - bounds.bottom); - } - if ('right' in bounds && 'left' in bounds) { - boundsWidth = Math.abs(bounds.right - bounds.left); - } - } - - this.calcOffset(); - pointer.x = pointer.x - this._offset.left; - pointer.y = pointer.y - this._offset.top; - if (!ignoreZoom) { - pointer = this.restorePointerVpt(pointer); - } - - var retinaScaling = this.getRetinaScaling(); - if (retinaScaling !== 1) { - pointer.x /= retinaScaling; - pointer.y /= retinaScaling; - } - - if (boundsWidth === 0 || boundsHeight === 0) { - // If bounds are not available (i.e. not visible), do not apply scale. - cssScale = { width: 1, height: 1 }; - } - else { - cssScale = { - width: upperCanvasEl.width / boundsWidth, - height: upperCanvasEl.height / boundsHeight - }; - } - - return { - x: pointer.x * cssScale.width, - y: pointer.y * cssScale.height - }; - }, - - /** - * @private - * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized - */ - _createUpperCanvas: function () { - var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''), - lowerCanvasEl = this.lowerCanvasEl, upperCanvasEl = this.upperCanvasEl; - - // there is no need to create a new upperCanvas element if we have already one. - if (upperCanvasEl) { - upperCanvasEl.className = ''; - } - else { - upperCanvasEl = this._createCanvasElement(); - this.upperCanvasEl = upperCanvasEl; - } - fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); - - this.wrapperEl.appendChild(upperCanvasEl); - - this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl); - this._applyCanvasStyle(upperCanvasEl); - this.contextTop = upperCanvasEl.getContext('2d'); - }, - - /** - * Returns context of top canvas where interactions are drawn - * @returns {CanvasRenderingContext2D} - */ - getTopContext: function () { - return this.contextTop; - }, - - /** - * @private - */ - _createCacheCanvas: function () { - this.cacheCanvasEl = this._createCanvasElement(); - this.cacheCanvasEl.setAttribute('width', this.width); - this.cacheCanvasEl.setAttribute('height', this.height); - this.contextCache = this.cacheCanvasEl.getContext('2d'); - }, - - /** - * @private - */ - _initWrapperElement: function () { - this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { - 'class': this.containerClass - }); - fabric.util.setStyle(this.wrapperEl, { - width: this.width + 'px', - height: this.height + 'px', - position: 'relative' - }); - fabric.util.makeElementUnselectable(this.wrapperEl); - }, - - /** - * @private - * @param {HTMLElement} element canvas element to apply styles on - */ - _applyCanvasStyle: function (element) { - var width = this.width || element.width, - height = this.height || element.height; - - fabric.util.setStyle(element, { - position: 'absolute', - width: width + 'px', - height: height + 'px', - left: 0, - top: 0, - 'touch-action': this.allowTouchScrolling ? 'manipulation' : 'none', - '-ms-touch-action': this.allowTouchScrolling ? 'manipulation' : 'none' - }); - element.width = width; - element.height = height; - fabric.util.makeElementUnselectable(element); - }, - - /** - * Copy the entire inline style from one element (fromEl) to another (toEl) - * @private - * @param {Element} fromEl Element style is copied from - * @param {Element} toEl Element copied style is applied to - */ - _copyCanvasStyle: function (fromEl, toEl) { - toEl.style.cssText = fromEl.style.cssText; - }, - - /** - * Returns context of canvas where object selection is drawn - * @return {CanvasRenderingContext2D} - */ - getSelectionContext: function () { - return this.contextTop; - }, - - /** - * Returns <canvas> element on which object selection is drawn - * @return {HTMLCanvasElement} - */ - getSelectionElement: function () { - return this.upperCanvasEl; - }, - - /** - * Returns currently active object - * @return {fabric.Object} active object - */ - getActiveObject: function () { - return this._activeObject; - }, - - /** - * Returns an array with the current selected objects - * @return {fabric.Object} active object - */ - getActiveObjects: function () { - var active = this._activeObject; - if (active) { - if (active.type === 'activeSelection' && active._objects) { - return active._objects.slice(0); - } - else { - return [active]; - } - } - return []; - }, - - /** - * @private - * @param {fabric.Object} obj Object that was removed - */ - _onObjectRemoved: function (obj) { - // removing active object should fire "selection:cleared" events - if (obj === this._activeObject) { - this.fire('before:selection:cleared', { target: obj }); - this._discardActiveObject(); - this.fire('selection:cleared', { target: obj }); - obj.fire('deselected'); - } - if (obj === this._hoveredTarget) { - this._hoveredTarget = null; - this._hoveredTargets = []; - } - this.callSuper('_onObjectRemoved', obj); - }, - - /** - * @private - * Compares the old activeObject with the current one and fires correct events - * @param {fabric.Object} obj old activeObject - */ - _fireSelectionEvents: function (oldObjects, e) { - var somethingChanged = false, objects = this.getActiveObjects(), - added = [], removed = []; - oldObjects.forEach(function (oldObject) { - if (objects.indexOf(oldObject) === -1) { - somethingChanged = true; - oldObject.fire('deselected', { - e: e, - target: oldObject - }); - removed.push(oldObject); - } - }); - objects.forEach(function (object) { - if (oldObjects.indexOf(object) === -1) { - somethingChanged = true; - object.fire('selected', { - e: e, - target: object - }); - added.push(object); - } - }); - if (oldObjects.length > 0 && objects.length > 0) { - somethingChanged && this.fire('selection:updated', { - e: e, - selected: added, - deselected: removed, - }); - } - else if (objects.length > 0) { - this.fire('selection:created', { - e: e, - selected: added, - }); - } - else if (oldObjects.length > 0) { - this.fire('selection:cleared', { - e: e, - deselected: removed, - }); - } - }, - - /** - * Sets given object as the only active object on canvas - * @param {fabric.Object} object Object to set as an active one - * @param {Event} [e] Event (passed along when firing "object:selected") - * @return {fabric.Canvas} thisArg - * @chainable - */ - setActiveObject: function (object, e) { - var currentActives = this.getActiveObjects(); - this._setActiveObject(object, e); - this._fireSelectionEvents(currentActives, e); - return this; - }, - - /** - * This is a private method for now. - * This is supposed to be equivalent to setActiveObject but without firing - * any event. There is commitment to have this stay this way. - * This is the functional part of setActiveObject. - * @private - * @param {Object} object to set as active - * @param {Event} [e] Event (passed along when firing "object:selected") - * @return {Boolean} true if the selection happened - */ - _setActiveObject: function (object, e) { - if (this._activeObject === object) { - return false; - } - if (!this._discardActiveObject(e, object)) { - return false; - } - if (object.onSelect({ e: e })) { - return false; - } - this._activeObject = object; - return true; - }, - - /** - * This is a private method for now. - * This is supposed to be equivalent to discardActiveObject but without firing - * any events. There is commitment to have this stay this way. - * This is the functional part of discardActiveObject. - * @param {Event} [e] Event (passed along when firing "object:deselected") - * @param {Object} object to set as active - * @return {Boolean} true if the selection happened - * @private - */ - _discardActiveObject: function (e, object) { - var obj = this._activeObject; - if (obj) { - // onDeselect return TRUE to cancel selection; - if (obj.onDeselect({ e: e, object: object })) { - return false; - } - this._activeObject = null; - } - return true; - }, - - /** - * Discards currently active object and fire events. If the function is called by fabric - * as a consequence of a mouse event, the event is passed as a parameter and - * sent to the fire function for the custom events. When used as a method the - * e param does not have any application. - * @param {event} e - * @return {fabric.Canvas} thisArg - * @chainable - */ - discardActiveObject: function (e) { - var currentActives = this.getActiveObjects(), activeObject = this.getActiveObject(); - if (currentActives.length) { - this.fire('before:selection:cleared', { target: activeObject, e: e }); - } - this._discardActiveObject(e); - this._fireSelectionEvents(currentActives, e); - return this; - }, - - /** - * Clears a canvas element and removes all event listeners - * @return {fabric.Canvas} thisArg - * @chainable - */ - dispose: function () { - var wrapper = this.wrapperEl; - this.removeListeners(); - wrapper.removeChild(this.upperCanvasEl); - wrapper.removeChild(this.lowerCanvasEl); - this.contextCache = null; - this.contextTop = null; - ['upperCanvasEl', 'cacheCanvasEl'].forEach((function (element) { - fabric.util.cleanUpJsdomNode(this[element]); - this[element] = undefined; - }).bind(this)); - if (wrapper.parentNode) { - wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); - } - delete this.wrapperEl; - fabric.StaticCanvas.prototype.dispose.call(this); - return this; - }, - - /** - * Clears all contexts (background, main, top) of an instance - * @return {fabric.Canvas} thisArg - * @chainable - */ - clear: function () { - // this.discardActiveGroup(); - this.discardActiveObject(); - this.clearContext(this.contextTop); - return this.callSuper('clear'); - }, - - /** - * Draws objects' controls (borders/controls) - * @param {CanvasRenderingContext2D} ctx Context to render controls on - */ - drawControls: function (ctx) { - var activeObject = this._activeObject; - - if (activeObject) { - activeObject._renderControls(ctx); - } - }, - - /** - * @private - */ - _toObject: function (instance, methodName, propertiesToInclude) { - //If the object is part of the current selection group, it should - //be transformed appropriately - //i.e. it should be serialised as it would appear if the selection group - //were to be destroyed. - var originalProperties = this._realizeGroupTransformOnObject(instance), - object = this.callSuper('_toObject', instance, methodName, propertiesToInclude); - //Undo the damage we did by changing all of its properties - this._unwindGroupTransformOnObject(instance, originalProperties); - return object; - }, - - /** - * Realises an object's group transformation on it - * @private - * @param {fabric.Object} [instance] the object to transform (gets mutated) - * @returns the original values of instance which were changed - */ - _realizeGroupTransformOnObject: function (instance) { - if (instance.group && instance.group.type === 'activeSelection' && this._activeObject === instance.group) { - var layoutProps = ['angle', 'flipX', 'flipY', 'left', 'scaleX', 'scaleY', 'skewX', 'skewY', 'top']; - //Copy all the positionally relevant properties across now - var originalValues = {}; - layoutProps.forEach(function (prop) { - originalValues[prop] = instance[prop]; - }); - fabric.util.addTransformToObject(instance, this._activeObject.calcOwnMatrix()); - return originalValues; - } - else { - return null; - } - }, - - /** - * Restores the changed properties of instance - * @private - * @param {fabric.Object} [instance] the object to un-transform (gets mutated) - * @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject - */ - _unwindGroupTransformOnObject: function (instance, originalValues) { - if (originalValues) { - instance.set(originalValues); - } - }, - - /** - * @private - */ - _setSVGObject: function (markup, instance, reviver) { - //If the object is in a selection group, simulate what would happen to that - //object when the group is deselected - var originalProperties = this._realizeGroupTransformOnObject(instance); - this.callSuper('_setSVGObject', markup, instance, reviver); - this._unwindGroupTransformOnObject(instance, originalProperties); - }, - - setViewportTransform: function (vpt) { - if (this.renderOnAddRemove && this._activeObject && this._activeObject.isEditing) { - this._activeObject.clearContextTop(); - } - fabric.StaticCanvas.prototype.setViewportTransform.call(this, vpt); - } - }); - - // copying static properties manually to work around Opera's bug, - // where "prototype" property is enumerable and overrides existing prototype - for (var prop in fabric.StaticCanvas) { - if (prop !== 'prototype') { - fabric.Canvas[prop] = fabric.StaticCanvas[prop]; - } - } -})(); - - -(function () { - - var addListener = fabric.util.addListener, - removeListener = fabric.util.removeListener, - RIGHT_CLICK = 3, MIDDLE_CLICK = 2, LEFT_CLICK = 1, - addEventOptions = { passive: false }; - - function checkClick(e, value) { - return e.button && (e.button === value - 1); - } - - fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { - - /** - * Contains the id of the touch event that owns the fabric transform - * @type Number - * @private - */ - mainTouchId: null, - - /** - * Adds mouse listeners to canvas - * @private - */ - _initEventListeners: function () { - // in case we initialized the class twice. This should not happen normally - // but in some kind of applications where the canvas element may be changed - // this is a workaround to having double listeners. - this.removeListeners(); - this._bindEvents(); - this.addOrRemove(addListener, 'add'); - }, - - /** - * return an event prefix pointer or mouse. - * @private - */ - _getEventPrefix: function () { - return this.enablePointerEvents ? 'pointer' : 'mouse'; - }, - - addOrRemove: function (functor, eventjsFunctor) { - var canvasElement = this.upperCanvasEl, - eventTypePrefix = this._getEventPrefix(); - functor(fabric.window, 'resize', this._onResize); - functor(canvasElement, eventTypePrefix + 'down', this._onMouseDown); - functor(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); - functor(canvasElement, eventTypePrefix + 'out', this._onMouseOut); - functor(canvasElement, eventTypePrefix + 'enter', this._onMouseEnter); - functor(canvasElement, 'wheel', this._onMouseWheel); - functor(canvasElement, 'contextmenu', this._onContextMenu); - functor(canvasElement, 'dblclick', this._onDoubleClick); - functor(canvasElement, 'dragover', this._onDragOver); - functor(canvasElement, 'dragenter', this._onDragEnter); - functor(canvasElement, 'dragleave', this._onDragLeave); - functor(canvasElement, 'drop', this._onDrop); - if (!this.enablePointerEvents) { - functor(canvasElement, 'touchstart', this._onTouchStart, addEventOptions); - } - if (typeof eventjs !== 'undefined' && eventjsFunctor in eventjs) { - eventjs[eventjsFunctor](canvasElement, 'gesture', this._onGesture); - eventjs[eventjsFunctor](canvasElement, 'drag', this._onDrag); - eventjs[eventjsFunctor](canvasElement, 'orientation', this._onOrientationChange); - eventjs[eventjsFunctor](canvasElement, 'shake', this._onShake); - eventjs[eventjsFunctor](canvasElement, 'longpress', this._onLongPress); - } - }, - - /** - * Removes all event listeners - */ - removeListeners: function () { - this.addOrRemove(removeListener, 'remove'); - // if you dispose on a mouseDown, before mouse up, you need to clean document to... - var eventTypePrefix = this._getEventPrefix(); - removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); - removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); - removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); - removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); - }, - - /** - * @private - */ - _bindEvents: function () { - if (this.eventsBound) { - // for any reason we pass here twice we do not want to bind events twice. - return; - } - this._onMouseDown = this._onMouseDown.bind(this); - this._onTouchStart = this._onTouchStart.bind(this); - this._onMouseMove = this._onMouseMove.bind(this); - this._onMouseUp = this._onMouseUp.bind(this); - this._onTouchEnd = this._onTouchEnd.bind(this); - this._onResize = this._onResize.bind(this); - this._onGesture = this._onGesture.bind(this); - this._onDrag = this._onDrag.bind(this); - this._onShake = this._onShake.bind(this); - this._onLongPress = this._onLongPress.bind(this); - this._onOrientationChange = this._onOrientationChange.bind(this); - this._onMouseWheel = this._onMouseWheel.bind(this); - this._onMouseOut = this._onMouseOut.bind(this); - this._onMouseEnter = this._onMouseEnter.bind(this); - this._onContextMenu = this._onContextMenu.bind(this); - this._onDoubleClick = this._onDoubleClick.bind(this); - this._onDragOver = this._onDragOver.bind(this); - this._onDragEnter = this._simpleEventHandler.bind(this, 'dragenter'); - this._onDragLeave = this._simpleEventHandler.bind(this, 'dragleave'); - this._onDrop = this._onDrop.bind(this); - this.eventsBound = true; - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js gesture - * @param {Event} [self] Inner Event object - */ - _onGesture: function (e, self) { - this.__onTransformGesture && this.__onTransformGesture(e, self); - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js drag - * @param {Event} [self] Inner Event object - */ - _onDrag: function (e, self) { - this.__onDrag && this.__onDrag(e, self); - }, - - /** - * @private - * @param {Event} [e] Event object fired on wheel event - */ - _onMouseWheel: function (e) { - this.__onMouseWheel(e); - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onMouseOut: function (e) { - var target = this._hoveredTarget; - this.fire('mouse:out', { target: target, e: e }); - this._hoveredTarget = null; - target && target.fire('mouseout', { e: e }); - - var _this = this; - this._hoveredTargets.forEach(function (_target) { - _this.fire('mouse:out', { target: target, e: e }); - _target && target.fire('mouseout', { e: e }); - }); - this._hoveredTargets = []; - }, - - /** - * @private - * @param {Event} e Event object fired on mouseenter - */ - _onMouseEnter: function (e) { - // This find target and consequent 'mouse:over' is used to - // clear old instances on hovered target. - // calling findTarget has the side effect of killing target.__corner. - // as a short term fix we are not firing this if we are currently transforming. - // as a long term fix we need to separate the action of finding a target with the - // side effects we added to it. - if (!this._currentTransform && !this.findTarget(e)) { - this.fire('mouse:over', { target: null, e: e }); - this._hoveredTarget = null; - this._hoveredTargets = []; - } - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js orientation change - * @param {Event} [self] Inner Event object - */ - _onOrientationChange: function (e, self) { - this.__onOrientationChange && this.__onOrientationChange(e, self); - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js shake - * @param {Event} [self] Inner Event object - */ - _onShake: function (e, self) { - this.__onShake && this.__onShake(e, self); - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js shake - * @param {Event} [self] Inner Event object - */ - _onLongPress: function (e, self) { - this.__onLongPress && this.__onLongPress(e, self); - }, - - /** - * prevent default to allow drop event to be fired - * @private - * @param {Event} [e] Event object fired on Event.js shake - */ - _onDragOver: function (e) { - e.preventDefault(); - var target = this._simpleEventHandler('dragover', e); - this._fireEnterLeaveEvents(target, e); - }, - - /** - * `drop:before` is a an event that allow you to schedule logic - * before the `drop` event. Prefer `drop` event always, but if you need - * to run some drop-disabling logic on an event, since there is no way - * to handle event handlers ordering, use `drop:before` - * @param {Event} e - */ - _onDrop: function (e) { - this._simpleEventHandler('drop:before', e); - return this._simpleEventHandler('drop', e); - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onContextMenu: function (e) { - if (this.stopContextMenu) { - e.stopPropagation(); - e.preventDefault(); - } - return false; - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onDoubleClick: function (e) { - this._cacheTransformEventData(e); - this._handleEvent(e, 'dblclick'); - this._resetTransformEventData(e); - }, - - /** - * Return a the id of an event. - * returns either the pointerId or the identifier or 0 for the mouse event - * @private - * @param {Event} evt Event object - */ - getPointerId: function (evt) { - var changedTouches = evt.changedTouches; - - if (changedTouches) { - return changedTouches[0] && changedTouches[0].identifier; - } - - if (this.enablePointerEvents) { - return evt.pointerId; - } - - return -1; - }, - - /** - * Determines if an event has the id of the event that is considered main - * @private - * @param {evt} event Event object - */ - _isMainEvent: function (evt) { - if (evt.isPrimary === true) { - return true; - } - if (evt.isPrimary === false) { - return false; - } - if (evt.type === 'touchend' && evt.touches.length === 0) { - return true; - } - if (evt.changedTouches) { - return evt.changedTouches[0].identifier === this.mainTouchId; - } - return true; - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onTouchStart: function (e) { - e.preventDefault(); - if (this.mainTouchId === null) { - this.mainTouchId = this.getPointerId(e); - } - this.__onMouseDown(e); - this._resetTransformEventData(); - var canvasElement = this.upperCanvasEl, - eventTypePrefix = this._getEventPrefix(); - addListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); - addListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); - // Unbind mousedown to prevent double triggers from touch devices - removeListener(canvasElement, eventTypePrefix + 'down', this._onMouseDown); - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onMouseDown: function (e) { - this.__onMouseDown(e); - this._resetTransformEventData(); - var canvasElement = this.upperCanvasEl, - eventTypePrefix = this._getEventPrefix(); - removeListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); - addListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); - addListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onTouchEnd: function (e) { - if (e.touches.length > 0) { - // if there are still touches stop here - return; - } - this.__onMouseUp(e); - this._resetTransformEventData(); - this.mainTouchId = null; - var eventTypePrefix = this._getEventPrefix(); - removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); - removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); - var _this = this; - if (this._willAddMouseDown) { - clearTimeout(this._willAddMouseDown); - } - this._willAddMouseDown = setTimeout(function () { - // Wait 400ms before rebinding mousedown to prevent double triggers - // from touch devices - addListener(_this.upperCanvasEl, eventTypePrefix + 'down', _this._onMouseDown); - _this._willAddMouseDown = 0; - }, 400); - }, - - /** - * @private - * @param {Event} e Event object fired on mouseup - */ - _onMouseUp: function (e) { - this.__onMouseUp(e); - this._resetTransformEventData(); - var canvasElement = this.upperCanvasEl, - eventTypePrefix = this._getEventPrefix(); - if (this._isMainEvent(e)) { - removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); - removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); - addListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); - } - }, - - /** - * @private - * @param {Event} e Event object fired on mousemove - */ - _onMouseMove: function (e) { - !this.allowTouchScrolling && e.preventDefault && e.preventDefault(); - this.__onMouseMove(e); - }, - - /** - * @private - */ - _onResize: function () { - this.calcOffset(); - }, - - /** - * Decides whether the canvas should be redrawn in mouseup and mousedown events. - * @private - * @param {Object} target - */ - _shouldRender: function (target) { - var activeObject = this._activeObject; - - if ( - !!activeObject !== !!target || - (activeObject && target && (activeObject !== target)) - ) { - // this covers: switch of target, from target to no target, selection of target - // multiSelection with key and mouse - return true; - } - else if (activeObject && activeObject.isEditing) { - // if we mouse up/down over a editing textbox a cursor change, - // there is no need to re render - return false; - } - return false; - }, - - /** - * Method that defines the actions when mouse is released on canvas. - * The method resets the currentTransform parameters, store the image corner - * position in the image object and render the canvas on top. - * @private - * @param {Event} e Event object fired on mouseup - */ - __onMouseUp: function (e) { - var target, transform = this._currentTransform, - groupSelector = this._groupSelector, shouldRender = false, - isClick = (!groupSelector || (groupSelector.left === 0 && groupSelector.top === 0)); - this._cacheTransformEventData(e); - target = this._target; - this._handleEvent(e, 'up:before'); - // if right/middle click just fire events and return - // target undefined will make the _handleEvent search the target - if (checkClick(e, RIGHT_CLICK)) { - if (this.fireRightClick) { - this._handleEvent(e, 'up', RIGHT_CLICK, isClick); - } - return; - } - - if (checkClick(e, MIDDLE_CLICK)) { - if (this.fireMiddleClick) { - this._handleEvent(e, 'up', MIDDLE_CLICK, isClick); - } - this._resetTransformEventData(); - return; - } - - if (this.isDrawingMode && this._isCurrentlyDrawing) { - this._onMouseUpInDrawingMode(e); - return; - } - - if (!this._isMainEvent(e)) { - return; - } - if (transform) { - this._finalizeCurrentTransform(e); - shouldRender = transform.actionPerformed; - } - if (!isClick) { - var targetWasActive = target === this._activeObject; - this._maybeGroupObjects(e); - if (!shouldRender) { - shouldRender = ( - this._shouldRender(target) || - (!targetWasActive && target === this._activeObject) - ); - } - } - var corner, pointer; - if (target) { - corner = target._findTargetCorner( - this.getPointer(e, true), - fabric.util.isTouchEvent(e) - ); - if (target.selectable && target !== this._activeObject && target.activeOn === 'up') { - this.setActiveObject(target, e); - shouldRender = true; - } - else { - var control = target.controls[corner], - mouseUpHandler = control && control.getMouseUpHandler(e, target, control); - if (mouseUpHandler) { - pointer = this.getPointer(e); - mouseUpHandler(e, transform, pointer.x, pointer.y); - } - } - target.isMoving = false; - } - // if we are ending up a transform on a different control or a new object - // fire the original mouse up from the corner that started the transform - if (transform && (transform.target !== target || transform.corner !== corner)) { - var originalControl = transform.target && transform.target.controls[transform.corner], - originalMouseUpHandler = originalControl && originalControl.getMouseUpHandler(e, target, control); - pointer = pointer || this.getPointer(e); - originalMouseUpHandler && originalMouseUpHandler(e, transform, pointer.x, pointer.y); - } - this._setCursorFromEvent(e, target); - this._handleEvent(e, 'up', LEFT_CLICK, isClick); - this._groupSelector = null; - this._currentTransform = null; - // reset the target information about which corner is selected - target && (target.__corner = 0); - if (shouldRender) { - this.requestRenderAll(); - } - else if (!isClick) { - this.renderTop(); - } - }, - - /** - * @private - * Handle event firing for target and subtargets - * @param {Event} e event from mouse - * @param {String} eventType event to fire (up, down or move) - * @return {Fabric.Object} target return the the target found, for internal reasons. - */ - _simpleEventHandler: function (eventType, e) { - var target = this.findTarget(e), - targets = this.targets, - options = { - e: e, - target: target, - subTargets: targets, - }; - this.fire(eventType, options); - target && target.fire(eventType, options); - if (!targets) { - return target; - } - for (var i = 0; i < targets.length; i++) { - targets[i].fire(eventType, options); - } - return target; - }, - - /** - * @private - * Handle event firing for target and subtargets - * @param {Event} e event from mouse - * @param {String} eventType event to fire (up, down or move) - * @param {fabric.Object} targetObj receiving event - * @param {Number} [button] button used in the event 1 = left, 2 = middle, 3 = right - * @param {Boolean} isClick for left button only, indicates that the mouse up happened without move. - */ - _handleEvent: function (e, eventType, button, isClick) { - var target = this._target, - targets = this.targets || [], - options = { - e: e, - target: target, - subTargets: targets, - button: button || LEFT_CLICK, - isClick: isClick || false, - pointer: this._pointer, - absolutePointer: this._absolutePointer, - transform: this._currentTransform - }; - if (eventType === 'up') { - options.currentTarget = this.findTarget(e); - options.currentSubTargets = this.targets; - } - this.fire('mouse:' + eventType, options); - target && target.fire('mouse' + eventType, options); - for (var i = 0; i < targets.length; i++) { - targets[i].fire('mouse' + eventType, options); - } - }, - - /** - * @private - * @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event - */ - _finalizeCurrentTransform: function (e) { - - var transform = this._currentTransform, - target = transform.target, - options = { - e: e, - target: target, - transform: transform, - action: transform.action, - }; - - if (target._scaling) { - target._scaling = false; - } - - target.setCoords(); - - if (transform.actionPerformed || (this.stateful && target.hasStateChanged())) { - this._fire('modified', options); - } - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onMouseDownInDrawingMode: function (e) { - this._isCurrentlyDrawing = true; - if (this.getActiveObject()) { - this.discardActiveObject(e).requestRenderAll(); - } - var pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseDown(pointer, { e: e, pointer: pointer }); - this._handleEvent(e, 'down'); - }, - - /** - * @private - * @param {Event} e Event object fired on mousemove - */ - _onMouseMoveInDrawingMode: function (e) { - if (this._isCurrentlyDrawing) { - var pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseMove(pointer, { e: e, pointer: pointer }); - } - this.setCursor(this.freeDrawingCursor); - this._handleEvent(e, 'move'); - }, - - /** - * @private - * @param {Event} e Event object fired on mouseup - */ - _onMouseUpInDrawingMode: function (e) { - var pointer = this.getPointer(e); - this._isCurrentlyDrawing = this.freeDrawingBrush.onMouseUp({ e: e, pointer: pointer }); - this._handleEvent(e, 'up'); - }, - - /** - * Method that defines the actions when mouse is clicked on canvas. - * The method inits the currentTransform parameters and renders all the - * canvas so the current image can be placed on the top canvas and the rest - * in on the container one. - * @private - * @param {Event} e Event object fired on mousedown - */ - __onMouseDown: function (e) { - this._cacheTransformEventData(e); - this._handleEvent(e, 'down:before'); - var target = this._target; - // if right click just fire events - if (checkClick(e, RIGHT_CLICK)) { - if (this.fireRightClick) { - this._handleEvent(e, 'down', RIGHT_CLICK); - } - return; - } - - if (checkClick(e, MIDDLE_CLICK)) { - if (this.fireMiddleClick) { - this._handleEvent(e, 'down', MIDDLE_CLICK); - } - return; - } - - if (this.isDrawingMode) { - this._onMouseDownInDrawingMode(e); - return; - } - - if (!this._isMainEvent(e)) { - return; - } - - // ignore if some object is being transformed at this moment - if (this._currentTransform) { - return; - } - - var pointer = this._pointer; - // save pointer for check in __onMouseUp event - this._previousPointer = pointer; - var shouldRender = this._shouldRender(target), - shouldGroup = this._shouldGroup(e, target); - if (this._shouldClearSelection(e, target)) { - this.discardActiveObject(e); - } - else if (shouldGroup) { - this._handleGrouping(e, target); - target = this._activeObject; - } - - if (this.selection && (!target || - (!target.selectable && !target.isEditing && target !== this._activeObject))) { - this._groupSelector = { - ex: this._absolutePointer.x, - ey: this._absolutePointer.y, - top: 0, - left: 0 - }; - } - - if (target) { - var alreadySelected = target === this._activeObject; - if (target.selectable && target.activeOn === 'down') { - this.setActiveObject(target, e); - } - var corner = target._findTargetCorner( - this.getPointer(e, true), - fabric.util.isTouchEvent(e) - ); - target.__corner = corner; - if (target === this._activeObject && (corner || !shouldGroup)) { - this._setupCurrentTransform(e, target, alreadySelected); - var control = target.controls[corner], - pointer = this.getPointer(e), - mouseDownHandler = control && control.getMouseDownHandler(e, target, control); - if (mouseDownHandler) { - mouseDownHandler(e, this._currentTransform, pointer.x, pointer.y); - } - } - } - this._handleEvent(e, 'down'); - // we must renderAll so that we update the visuals - (shouldRender || shouldGroup) && this.requestRenderAll(); - }, - - /** - * reset cache form common information needed during event processing - * @private - */ - _resetTransformEventData: function () { - this._target = null; - this._pointer = null; - this._absolutePointer = null; - }, - - /** - * Cache common information needed during event processing - * @private - * @param {Event} e Event object fired on event - */ - _cacheTransformEventData: function (e) { - // reset in order to avoid stale caching - this._resetTransformEventData(); - this._pointer = this.getPointer(e, true); - this._absolutePointer = this.restorePointerVpt(this._pointer); - this._target = this._currentTransform ? this._currentTransform.target : this.findTarget(e) || null; - }, - - /** - * @private - */ - _beforeTransform: function (e) { - var t = this._currentTransform; - this.stateful && t.target.saveState(); - this.fire('before:transform', { - e: e, - transform: t, - }); - }, - - /** - * Method that defines the actions when mouse is hovering the canvas. - * The currentTransform parameter will define whether the user is rotating/scaling/translating - * an image or neither of them (only hovering). A group selection is also possible and would cancel - * all any other type of action. - * In case of an image transformation only the top canvas will be rendered. - * @private - * @param {Event} e Event object fired on mousemove - */ - __onMouseMove: function (e) { - this._handleEvent(e, 'move:before'); - this._cacheTransformEventData(e); - var target, pointer; - - if (this.isDrawingMode) { - this._onMouseMoveInDrawingMode(e); - return; - } - - if (!this._isMainEvent(e)) { - return; - } - - var groupSelector = this._groupSelector; - - // We initially clicked in an empty area, so we draw a box for multiple selection - if (groupSelector) { - pointer = this._absolutePointer; - - groupSelector.left = pointer.x - groupSelector.ex; - groupSelector.top = pointer.y - groupSelector.ey; - - this.renderTop(); - } - else if (!this._currentTransform) { - target = this.findTarget(e) || null; - this._setCursorFromEvent(e, target); - this._fireOverOutEvents(target, e); - } - else { - this._transformObject(e); - } - this._handleEvent(e, 'move'); - this._resetTransformEventData(); - }, - - /** - * Manage the mouseout, mouseover events for the fabric object on the canvas - * @param {Fabric.Object} target the target where the target from the mousemove event - * @param {Event} e Event object fired on mousemove - * @private - */ - _fireOverOutEvents: function (target, e) { - var _hoveredTarget = this._hoveredTarget, - _hoveredTargets = this._hoveredTargets, targets = this.targets, - length = Math.max(_hoveredTargets.length, targets.length); - - this.fireSyntheticInOutEvents(target, e, { - oldTarget: _hoveredTarget, - evtOut: 'mouseout', - canvasEvtOut: 'mouse:out', - evtIn: 'mouseover', - canvasEvtIn: 'mouse:over', - }); - for (var i = 0; i < length; i++) { - this.fireSyntheticInOutEvents(targets[i], e, { - oldTarget: _hoveredTargets[i], - evtOut: 'mouseout', - evtIn: 'mouseover', - }); - } - this._hoveredTarget = target; - this._hoveredTargets = this.targets.concat(); - }, - - /** - * Manage the dragEnter, dragLeave events for the fabric objects on the canvas - * @param {Fabric.Object} target the target where the target from the onDrag event - * @param {Event} e Event object fired on ondrag - * @private - */ - _fireEnterLeaveEvents: function (target, e) { - var _draggedoverTarget = this._draggedoverTarget, - _hoveredTargets = this._hoveredTargets, targets = this.targets, - length = Math.max(_hoveredTargets.length, targets.length); - - this.fireSyntheticInOutEvents(target, e, { - oldTarget: _draggedoverTarget, - evtOut: 'dragleave', - evtIn: 'dragenter', - }); - for (var i = 0; i < length; i++) { - this.fireSyntheticInOutEvents(targets[i], e, { - oldTarget: _hoveredTargets[i], - evtOut: 'dragleave', - evtIn: 'dragenter', - }); - } - this._draggedoverTarget = target; - }, - - /** - * Manage the synthetic in/out events for the fabric objects on the canvas - * @param {Fabric.Object} target the target where the target from the supported events - * @param {Event} e Event object fired - * @param {Object} config configuration for the function to work - * @param {String} config.targetName property on the canvas where the old target is stored - * @param {String} [config.canvasEvtOut] name of the event to fire at canvas level for out - * @param {String} config.evtOut name of the event to fire for out - * @param {String} [config.canvasEvtIn] name of the event to fire at canvas level for in - * @param {String} config.evtIn name of the event to fire for in - * @private - */ - fireSyntheticInOutEvents: function (target, e, config) { - var inOpt, outOpt, oldTarget = config.oldTarget, outFires, inFires, - targetChanged = oldTarget !== target, canvasEvtIn = config.canvasEvtIn, canvasEvtOut = config.canvasEvtOut; - if (targetChanged) { - inOpt = { e: e, target: target, previousTarget: oldTarget }; - outOpt = { e: e, target: oldTarget, nextTarget: target }; - } - inFires = target && targetChanged; - outFires = oldTarget && targetChanged; - if (outFires) { - canvasEvtOut && this.fire(canvasEvtOut, outOpt); - oldTarget.fire(config.evtOut, outOpt); - } - if (inFires) { - canvasEvtIn && this.fire(canvasEvtIn, inOpt); - target.fire(config.evtIn, inOpt); - } - }, - - /** - * Method that defines actions when an Event Mouse Wheel - * @param {Event} e Event object fired on mouseup - */ - __onMouseWheel: function (e) { - this._cacheTransformEventData(e); - this._handleEvent(e, 'wheel'); - this._resetTransformEventData(); - }, - - /** - * @private - * @param {Event} e Event fired on mousemove - */ - _transformObject: function (e) { - var pointer = this.getPointer(e), - transform = this._currentTransform; - - transform.reset = false; - transform.shiftKey = e.shiftKey; - transform.altKey = e[this.centeredKey]; - - this._performTransformAction(e, transform, pointer); - transform.actionPerformed && this.requestRenderAll(); - }, - - /** - * @private - */ - _performTransformAction: function (e, transform, pointer) { - var x = pointer.x, - y = pointer.y, - action = transform.action, - actionPerformed = false, - actionHandler = transform.actionHandler; - // this object could be created from the function in the control handlers - - - if (actionHandler) { - actionPerformed = actionHandler(e, transform, x, y); - } - if (action === 'drag' && actionPerformed) { - transform.target.isMoving = true; - this.setCursor(transform.target.moveCursor || this.moveCursor); - } - transform.actionPerformed = transform.actionPerformed || actionPerformed; - }, - - /** - * @private - */ - _fire: fabric.controlsUtils.fireEvent, - - /** - * Sets the cursor depending on where the canvas is being hovered. - * Note: very buggy in Opera - * @param {Event} e Event object - * @param {Object} target Object that the mouse is hovering, if so. - */ - _setCursorFromEvent: function (e, target) { - if (!target) { - this.setCursor(this.defaultCursor); - return false; - } - var hoverCursor = target.hoverCursor || this.hoverCursor, - activeSelection = this._activeObject && this._activeObject.type === 'activeSelection' ? - this._activeObject : null, - // only show proper corner when group selection is not active - corner = (!activeSelection || !activeSelection.contains(target)) - // here we call findTargetCorner always with undefined for the touch parameter. - // we assume that if you are using a cursor you do not need to interact with - // the bigger touch area. - && target._findTargetCorner(this.getPointer(e, true)); - - if (!corner) { - if (target.subTargetCheck) { - // hoverCursor should come from top-most subTarget, - // so we walk the array backwards - this.targets.concat().reverse().map(function (_target) { - hoverCursor = _target.hoverCursor || hoverCursor; - }); - } - this.setCursor(hoverCursor); - } - else { - this.setCursor(this.getCornerCursor(corner, target, e)); - } - }, - - /** - * @private - */ - getCornerCursor: function (corner, target, e) { - var control = target.controls[corner]; - return control.cursorStyleHandler(e, control, target); - } - }); -})(); - - -(function () { - - var min = Math.min, - max = Math.max; - - fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { - - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - * @return {Boolean} - */ - _shouldGroup: function (e, target) { - var activeObject = this._activeObject; - return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection && - (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e }); - }, - - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - */ - _handleGrouping: function (e, target) { - var activeObject = this._activeObject; - // avoid multi select when shift click on a corner - if (activeObject.__corner) { - return; - } - if (target === activeObject) { - // if it's a group, find target again, using activeGroup objects - target = this.findTarget(e, true); - // if even object is not found or we are on activeObjectCorner, bail out - if (!target || !target.selectable) { - return; - } - } - if (activeObject && activeObject.type === 'activeSelection') { - this._updateActiveSelection(target, e); - } - else { - this._createActiveSelection(target, e); - } - }, - - /** - * @private - */ - _updateActiveSelection: function (target, e) { - var activeSelection = this._activeObject, - currentActiveObjects = activeSelection._objects.slice(0); - if (activeSelection.contains(target)) { - activeSelection.removeWithUpdate(target); - this._hoveredTarget = target; - this._hoveredTargets = this.targets.concat(); - if (activeSelection.size() === 1) { - // activate last remaining object - this._setActiveObject(activeSelection.item(0), e); - } - } - else { - activeSelection.addWithUpdate(target); - this._hoveredTarget = activeSelection; - this._hoveredTargets = this.targets.concat(); - } - this._fireSelectionEvents(currentActiveObjects, e); - }, - - /** - * @private - */ - _createActiveSelection: function (target, e) { - var currentActives = this.getActiveObjects(), group = this._createGroup(target); - this._hoveredTarget = group; - // ISSUE 4115: should we consider subTargets here? - // this._hoveredTargets = []; - // this._hoveredTargets = this.targets.concat(); - this._setActiveObject(group, e); - this._fireSelectionEvents(currentActives, e); - }, - - /** - * @private - * @param {Object} target - */ - _createGroup: function (target) { - var objects = this._objects, - isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), - groupObjects = isActiveLower - ? [this._activeObject, target] - : [target, this._activeObject]; - this._activeObject.isEditing && this._activeObject.exitEditing(); - return new fabric.ActiveSelection(groupObjects, { - canvas: this - }); - }, - - /** - * @private - * @param {Event} e mouse event - */ - _groupSelectedObjects: function (e) { - - var group = this._collectObjects(e), - aGroup; - - // do not create group for 1 element only - if (group.length === 1) { - this.setActiveObject(group[0], e); - } - else if (group.length > 1) { - aGroup = new fabric.ActiveSelection(group.reverse(), { - canvas: this - }); - this.setActiveObject(aGroup, e); - } - }, - - /** - * @private - */ - _collectObjects: function (e) { - var group = [], - currentObject, - x1 = this._groupSelector.ex, - y1 = this._groupSelector.ey, - x2 = x1 + this._groupSelector.left, - y2 = y1 + this._groupSelector.top, - selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), - selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), - allowIntersect = !this.selectionFullyContained, - isClick = x1 === x2 && y1 === y2; - // we iterate reverse order to collect top first in case of click. - for (var i = this._objects.length; i--;) { - currentObject = this._objects[i]; - - if (!currentObject || !currentObject.selectable || !currentObject.visible) { - continue; - } - - if ((allowIntersect && currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2, true)) || - currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2, true) || - (allowIntersect && currentObject.containsPoint(selectionX1Y1, null, true)) || - (allowIntersect && currentObject.containsPoint(selectionX2Y2, null, true)) - ) { - group.push(currentObject); - // only add one object if it's a click - if (isClick) { - break; - } - } - } - - if (group.length > 1) { - group = group.filter(function (object) { - return !object.onSelect({ e: e }); - }); - } - - return group; - }, - - /** - * @private - */ - _maybeGroupObjects: function (e) { - if (this.selection && this._groupSelector) { - this._groupSelectedObjects(e); - } - this.setCursor(this.defaultCursor); - // clear selection and current transformation - this._groupSelector = null; - } - }); - -})(); - - -(function () { - fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { - - /** - * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately - * @param {Object} [options] Options object - * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" - * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. - * @param {Number} [options.multiplier=1] Multiplier to scale by, to have consistent - * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 - * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 - * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 - * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 - * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 - * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format - * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} - * @example Generate jpeg dataURL with lower quality - * var dataURL = canvas.toDataURL({ - * format: 'jpeg', - * quality: 0.8 - * }); - * @example Generate cropped png dataURL (clipping of canvas) - * var dataURL = canvas.toDataURL({ - * format: 'png', - * left: 100, - * top: 100, - * width: 200, - * height: 200 - * }); - * @example Generate double scaled png dataURL - * var dataURL = canvas.toDataURL({ - * format: 'png', - * multiplier: 2 - * }); - */ - toDataURL: function (options) { - options || (options = {}); - - var format = options.format || 'png', - quality = options.quality || 1, - multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? this.getRetinaScaling() : 1), - canvasEl = this.toCanvasElement(multiplier, options); - return fabric.util.toDataURL(canvasEl, format, quality); - }, - - /** - * Create a new HTMLCanvas element painted with the current canvas content. - * No need to resize the actual one or repaint it. - * Will transfer object ownership to a new canvas, paint it, and set everything back. - * This is an intermediary step used to get to a dataUrl but also it is useful to - * create quick image copies of a canvas without passing for the dataUrl string - * @param {Number} [multiplier] a zoom factor. - * @param {Object} [cropping] Cropping informations - * @param {Number} [cropping.left] Cropping left offset. - * @param {Number} [cropping.top] Cropping top offset. - * @param {Number} [cropping.width] Cropping width. - * @param {Number} [cropping.height] Cropping height. - */ - toCanvasElement: function (multiplier, cropping) { - multiplier = multiplier || 1; - cropping = cropping || {}; - var scaledWidth = (cropping.width || this.width) * multiplier, - scaledHeight = (cropping.height || this.height) * multiplier, - zoom = this.getZoom(), - originalWidth = this.width, - originalHeight = this.height, - newZoom = zoom * multiplier, - vp = this.viewportTransform, - translateX = (vp[4] - (cropping.left || 0)) * multiplier, - translateY = (vp[5] - (cropping.top || 0)) * multiplier, - originalInteractive = this.interactive, - newVp = [newZoom, 0, 0, newZoom, translateX, translateY], - originalRetina = this.enableRetinaScaling, - canvasEl = fabric.util.createCanvasElement(), - originalContextTop = this.contextTop; - canvasEl.width = scaledWidth; - canvasEl.height = scaledHeight; - this.contextTop = null; - this.enableRetinaScaling = false; - this.interactive = false; - this.viewportTransform = newVp; - this.width = scaledWidth; - this.height = scaledHeight; - this.calcViewportBoundaries(); - this.renderCanvas(canvasEl.getContext('2d'), this._objects); - this.viewportTransform = vp; - this.width = originalWidth; - this.height = originalHeight; - this.calcViewportBoundaries(); - this.interactive = originalInteractive; - this.enableRetinaScaling = originalRetina; - this.contextTop = originalContextTop; - return canvasEl; - }, - }); - -})(); - - -fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { - /** - * Populates canvas with data from the specified JSON. - * JSON format must conform to the one of {@link fabric.Canvas#toJSON} - * @param {String|Object} json JSON string or object - * @param {Function} callback Callback, invoked when json is parsed - * and corresponding objects (e.g: {@link fabric.Image}) - * are initialized - * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. - * @return {fabric.Canvas} instance - * @chainable - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} - * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} - * @example loadFromJSON - * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); - * @example loadFromJSON with reviver - * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { - * // `o` = json object - * // `object` = fabric.Object instance - * // ... do some stuff ... - * }); - */ - loadFromJSON: function (json, callback, reviver) { - if (!json) { - return; - } - - // serialize if it wasn't already - var serialized = (typeof json === 'string') - ? JSON.parse(json) - : fabric.util.object.clone(json); - - var _this = this, - clipPath = serialized.clipPath, - renderOnAddRemove = this.renderOnAddRemove; - - this.renderOnAddRemove = false; - - delete serialized.clipPath; - - this._enlivenObjects(serialized.objects, function (enlivenedObjects) { - _this.clear(); - _this._setBgOverlay(serialized, function () { - if (clipPath) { - _this._enlivenObjects([clipPath], function (enlivenedCanvasClip) { - _this.clipPath = enlivenedCanvasClip[0]; - _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); - }); - } - else { - _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); - } - }); - }, reviver); - return this; - }, - - /** - * @private - * @param {Object} serialized Object with background and overlay information - * @param {Array} restored canvas objects - * @param {Function} cached renderOnAddRemove callback - * @param {Function} callback Invoked after all background and overlay images/patterns loaded - */ - __setupCanvas: function (serialized, enlivenedObjects, renderOnAddRemove, callback) { - var _this = this; - enlivenedObjects.forEach(function (obj, index) { - // we splice the array just in case some custom classes restored from JSON - // will add more object to canvas at canvas init. - _this.insertAt(obj, index); - }); - this.renderOnAddRemove = renderOnAddRemove; - // remove parts i cannot set as options - delete serialized.objects; - delete serialized.backgroundImage; - delete serialized.overlayImage; - delete serialized.background; - delete serialized.overlay; - // this._initOptions does too many things to just - // call it. Normally loading an Object from JSON - // create the Object instance. Here the Canvas is - // already an instance and we are just loading things over it - this._setOptions(serialized); - this.renderAll(); - callback && callback(); - }, - - /** - * @private - * @param {Object} serialized Object with background and overlay information - * @param {Function} callback Invoked after all background and overlay images/patterns loaded - */ - _setBgOverlay: function (serialized, callback) { - var loaded = { - backgroundColor: false, - overlayColor: false, - backgroundImage: false, - overlayImage: false - }; - - if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { - callback && callback(); - return; - } - - var cbIfLoaded = function () { - if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { - callback && callback(); - } - }; - - this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); - this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); - this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); - this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); - }, - - /** - * @private - * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) - * @param {(Object|String)} value Value to set - * @param {Object} loaded Set loaded property to true if property is set - * @param {Object} callback Callback function to invoke after property is set - */ - __setBgOverlay: function (property, value, loaded, callback) { - var _this = this; - - if (!value) { - loaded[property] = true; - callback && callback(); - return; - } - - if (property === 'backgroundImage' || property === 'overlayImage') { - fabric.util.enlivenObjects([value], function (enlivedObject) { - _this[property] = enlivedObject[0]; - loaded[property] = true; - callback && callback(); - }); - } - else { - this['set' + fabric.util.string.capitalize(property, true)](value, function () { - loaded[property] = true; - callback && callback(); - }); - } - }, - - /** - * @private - * @param {Array} objects - * @param {Function} callback - * @param {Function} [reviver] - */ - _enlivenObjects: function (objects, callback, reviver) { - if (!objects || objects.length === 0) { - callback && callback([]); - return; - } - - fabric.util.enlivenObjects(objects, function (enlivenedObjects) { - callback && callback(enlivenedObjects); - }, null, reviver); - }, - - /** - * @private - * @param {String} format - * @param {Function} callback - */ - _toDataURL: function (format, callback) { - this.clone(function (clone) { - callback(clone.toDataURL(format)); - }); - }, - - /** - * @private - * @param {String} format - * @param {Number} multiplier - * @param {Function} callback - */ - _toDataURLWithMultiplier: function (format, multiplier, callback) { - this.clone(function (clone) { - callback(clone.toDataURLWithMultiplier(format, multiplier)); - }); - }, - - /** - * Clones canvas instance - * @param {Object} [callback] Receives cloned instance as a first argument - * @param {Array} [properties] Array of properties to include in the cloned canvas and children - */ - clone: function (callback, properties) { - var data = JSON.stringify(this.toJSON(properties)); - this.cloneWithoutData(function (clone) { - clone.loadFromJSON(data, function () { - callback && callback(clone); - }); - }); - }, - - /** - * Clones canvas instance without cloning existing data. - * This essentially copies canvas dimensions, clipping properties, etc. - * but leaves data empty (so that you can populate it with your own) - * @param {Object} [callback] Receives cloned instance as a first argument - */ - cloneWithoutData: function (callback) { - var el = fabric.util.createCanvasElement(); - - el.width = this.width; - el.height = this.height; - - var clone = new fabric.Canvas(el); - if (this.backgroundImage) { - clone.setBackgroundImage(this.backgroundImage.src, function () { - clone.renderAll(); - callback && callback(clone); - }); - clone.backgroundImageOpacity = this.backgroundImageOpacity; - clone.backgroundImageStretch = this.backgroundImageStretch; - } - else { - callback && callback(clone); - } - } -}); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - extend = fabric.util.object.extend, - clone = fabric.util.object.clone, - toFixed = fabric.util.toFixed, - capitalize = fabric.util.string.capitalize, - degreesToRadians = fabric.util.degreesToRadians, - objectCaching = !fabric.isLikelyNode, - ALIASING_LIMIT = 2; - - if (fabric.Object) { - return; - } - - /** - * Root object class from which all 2d shape classes inherit from - * @class fabric.Object - * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#objects} - * @see {@link fabric.Object#initialize} for constructor definition - * - * @fires added - * @fires removed - * - * @fires selected - * @fires deselected - * @fires modified - * @fires modified - * @fires moved - * @fires scaled - * @fires rotated - * @fires skewed - * - * @fires rotating - * @fires scaling - * @fires moving - * @fires skewing - * - * @fires mousedown - * @fires mouseup - * @fires mouseover - * @fires mouseout - * @fires mousewheel - * @fires mousedblclick - * - * @fires dragover - * @fires dragenter - * @fires dragleave - * @fires drop - */ - fabric.Object = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.Object.prototype */ { - - /** - * Type of an object (rect, circle, path, etc.). - * Note that this property is meant to be read-only and not meant to be modified. - * If you modify, certain parts of Fabric (such as JSON loading) won't work correctly. - * @type String - * @default - */ - type: 'object', - - /** - * Horizontal origin of transformation of an object (one of "left", "right", "center") - * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups - * @type String - * @default - */ - originX: 'left', - - /** - * Vertical origin of transformation of an object (one of "top", "bottom", "center") - * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups - * @type String - * @default - */ - originY: 'top', - - /** - * Top position of an object. Note that by default it's relative to object top. You can change this by setting originY={top/center/bottom} - * @type Number - * @default - */ - top: 0, - - /** - * Left position of an object. Note that by default it's relative to object left. You can change this by setting originX={left/center/right} - * @type Number - * @default - */ - left: 0, - - /** - * Object width - * @type Number - * @default - */ - width: 0, - - /** - * Object height - * @type Number - * @default - */ - height: 0, - - /** - * Object scale factor (horizontal) - * @type Number - * @default - */ - scaleX: 1, - - /** - * Object scale factor (vertical) - * @type Number - * @default - */ - scaleY: 1, - - /** - * When true, an object is rendered as flipped horizontally - * @type Boolean - * @default - */ - flipX: false, - - /** - * When true, an object is rendered as flipped vertically - * @type Boolean - * @default - */ - flipY: false, - - /** - * Opacity of an object - * @type Number - * @default - */ - opacity: 1, - - /** - * Angle of rotation of an object (in degrees) - * @type Number - * @default - */ - angle: 0, - - /** - * Angle of skew on x axes of an object (in degrees) - * @type Number - * @default - */ - skewX: 0, - - /** - * Angle of skew on y axes of an object (in degrees) - * @type Number - * @default - */ - skewY: 0, - - /** - * Size of object's controlling corners (in pixels) - * @type Number - * @default - */ - cornerSize: 13, - - /** - * Size of object's controlling corners when touch interaction is detected - * @type Number - * @default - */ - touchCornerSize: 24, - - /** - * When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill) - * @type Boolean - * @default - */ - transparentCorners: true, - - /** - * Default cursor value used when hovering over this object on canvas - * @type String - * @default - */ - hoverCursor: null, - - /** - * Default cursor value used when moving this object on canvas - * @type String - * @default - */ - moveCursor: null, - - /** - * Padding between object and its controlling borders (in pixels) - * @type Number - * @default - */ - padding: 0, - - /** - * Color of controlling borders of an object (when it's active) - * @type String - * @default - */ - borderColor: 'rgb(178,204,255)', - - /** - * Array specifying dash pattern of an object's borders (hasBorder must be true) - * @since 1.6.2 - * @type Array - */ - borderDashArray: null, - - /** - * Color of controlling corners of an object (when it's active) - * @type String - * @default - */ - cornerColor: 'rgb(178,204,255)', - - /** - * Color of controlling corners of an object (when it's active and transparentCorners false) - * @since 1.6.2 - * @type String - * @default - */ - cornerStrokeColor: null, - - /** - * Specify style of control, 'rect' or 'circle' - * @since 1.6.2 - * @type String - */ - cornerStyle: 'rect', - - /** - * Array specifying dash pattern of an object's control (hasBorder must be true) - * @since 1.6.2 - * @type Array - */ - cornerDashArray: null, - - /** - * When true, this object will use center point as the origin of transformation - * when being scaled via the controls. - * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). - * @since 1.3.4 - * @type Boolean - * @default - */ - centeredScaling: false, - - /** - * When true, this object will use center point as the origin of transformation - * when being rotated via the controls. - * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). - * @since 1.3.4 - * @type Boolean - * @default - */ - centeredRotation: true, - - /** - * Color of object's fill - * takes css colors https://www.w3.org/TR/css-color-3/ - * @type String - * @default - */ - fill: 'rgb(0,0,0)', - - /** - * Fill rule used to fill an object - * accepted values are nonzero, evenodd - * Backwards incompatibility note: This property was used for setting globalCompositeOperation until v1.4.12 (use `fabric.Object#globalCompositeOperation` instead) - * @type String - * @default - */ - fillRule: 'nonzero', - - /** - * Composite rule used for canvas globalCompositeOperation - * @type String - * @default - */ - globalCompositeOperation: 'source-over', - - /** - * Background color of an object. - * takes css colors https://www.w3.org/TR/css-color-3/ - * @type String - * @default - */ - backgroundColor: '', - - /** - * Selection Background color of an object. colored layer behind the object when it is active. - * does not mix good with globalCompositeOperation methods. - * @type String - * @default - */ - selectionBackgroundColor: '', - - /** - * When defined, an object is rendered via stroke and this property specifies its color - * takes css colors https://www.w3.org/TR/css-color-3/ - * @type String - * @default - */ - stroke: null, - - /** - * Width of a stroke used to render this object - * @type Number - * @default - */ - strokeWidth: 1, - - /** - * Array specifying dash pattern of an object's stroke (stroke must be defined) - * @type Array - */ - strokeDashArray: null, - - /** - * Line offset of an object's stroke - * @type Number - * @default - */ - strokeDashOffset: 0, - - /** - * Line endings style of an object's stroke (one of "butt", "round", "square") - * @type String - * @default - */ - strokeLineCap: 'butt', - - /** - * Corner style of an object's stroke (one of "bevel", "round", "miter") - * @type String - * @default - */ - strokeLineJoin: 'miter', - - /** - * Maximum miter length (used for strokeLineJoin = "miter") of an object's stroke - * @type Number - * @default - */ - strokeMiterLimit: 4, - - /** - * Shadow object representing shadow of this shape - * @type fabric.Shadow - * @default - */ - shadow: null, - - /** - * Opacity of object's controlling borders when object is active and moving - * @type Number - * @default - */ - borderOpacityWhenMoving: 0.4, - - /** - * Scale factor of object's controlling borders - * bigger number will make a thicker border - * border is 1, so this is basically a border thickness - * since there is no way to change the border itself. - * @type Number - * @default - */ - borderScaleFactor: 1, - - /** - * Minimum allowed scale value of an object - * @type Number - * @default - */ - minScaleLimit: 0, - - /** - * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection). - * But events still fire on it. - * @type Boolean - * @default - */ - selectable: true, - - /** - * When set to `false`, an object can not be a target of events. All events propagate through it. Introduced in v1.3.4 - * @type Boolean - * @default - */ - evented: true, - - /** - * When set to `false`, an object is not rendered on canvas - * @type Boolean - * @default - */ - visible: true, - - /** - * When set to `false`, object's controls are not displayed and can not be used to manipulate object - * @type Boolean - * @default - */ - hasControls: true, - - /** - * When set to `false`, object's controlling borders are not rendered - * @type Boolean - * @default - */ - hasBorders: true, - - /** - * When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box - * @type Boolean - * @default - */ - perPixelTargetFind: false, - - /** - * When `false`, default object's values are not included in its serialization - * @type Boolean - * @default - */ - includeDefaultValues: true, - - /** - * When `true`, object horizontal movement is locked - * @type Boolean - * @default - */ - lockMovementX: false, - - /** - * When `true`, object vertical movement is locked - * @type Boolean - * @default - */ - lockMovementY: false, - - /** - * When `true`, object rotation is locked - * @type Boolean - * @default - */ - lockRotation: false, - - /** - * When `true`, object horizontal scaling is locked - * @type Boolean - * @default - */ - lockScalingX: false, - - /** - * When `true`, object vertical scaling is locked - * @type Boolean - * @default - */ - lockScalingY: false, - - /** - * When `true`, object horizontal skewing is locked - * @type Boolean - * @default - */ - lockSkewingX: false, - - /** - * When `true`, object vertical skewing is locked - * @type Boolean - * @default - */ - lockSkewingY: false, - - /** - * When `true`, object cannot be flipped by scaling into negative values - * @type Boolean - * @default - */ - lockScalingFlip: false, - - /** - * When `true`, object is not exported in OBJECT/JSON - * @since 1.6.3 - * @type Boolean - * @default - */ - excludeFromExport: false, - - /** - * When `true`, object is cached on an additional canvas. - * When `false`, object is not cached unless necessary ( clipPath ) - * default to true - * @since 1.7.0 - * @type Boolean - * @default true - */ - objectCaching: objectCaching, - - /** - * When `true`, object properties are checked for cache invalidation. In some particular - * situation you may want this to be disabled ( spray brush, very big, groups) - * or if your application does not allow you to modify properties for groups child you want - * to disable it for groups. - * default to false - * since 1.7.0 - * @type Boolean - * @default false - */ - statefullCache: false, - - /** - * When `true`, cache does not get updated during scaling. The picture will get blocky if scaled - * too much and will be redrawn with correct details at the end of scaling. - * this setting is performance and application dependant. - * default to true - * since 1.7.0 - * @type Boolean - * @default true - */ - noScaleCache: true, - - /** - * When `false`, the stoke width will scale with the object. - * When `true`, the stroke will always match the exact pixel size entered for stroke width. - * this Property does not work on Text classes or drawing call that uses strokeText,fillText methods - * default to false - * @since 2.6.0 - * @type Boolean - * @default false - * @type Boolean - * @default false - */ - strokeUniform: false, - - /** - * When set to `true`, object's cache will be rerendered next render call. - * since 1.7.0 - * @type Boolean - * @default true - */ - dirty: true, - - /** - * keeps the value of the last hovered corner during mouse move. - * 0 is no corner, or 'mt', 'ml', 'mtr' etc.. - * It should be private, but there is no harm in using it as - * a read-only property. - * @type number|string|any - * @default 0 - */ - __corner: 0, - - /** - * Determines if the fill or the stroke is drawn first (one of "fill" or "stroke") - * @type String - * @default - */ - paintFirst: 'fill', - - /** - * When 'down', object is set to active on mousedown/touchstart - * When 'up', object is set to active on mouseup/touchend - * Experimental. Let's see if this breaks anything before supporting officially - * @private - * since 4.4.0 - * @type String - * @default 'down' - */ - activeOn: 'down', - - /** - * List of properties to consider when checking if state - * of an object is changed (fabric.Object#hasStateChanged) - * as well as for history (undo/redo) purposes - * @type Array - */ - stateProperties: ( - 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + - 'stroke strokeWidth strokeDashArray strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit ' + - 'angle opacity fill globalCompositeOperation shadow visible backgroundColor ' + - 'skewX skewY fillRule paintFirst clipPath strokeUniform' - ).split(' '), - - /** - * List of properties to consider when checking if cache needs refresh - * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single - * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty - * and refreshed at the next render - * @type Array - */ - cacheProperties: ( - 'fill stroke strokeWidth strokeDashArray width height paintFirst strokeUniform' + - ' strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit backgroundColor clipPath' - ).split(' '), - - /** - * List of properties to consider for animating colors. - * @type Array - */ - colorProperties: ( - 'fill stroke backgroundColor' - ).split(' '), - - /** - * a fabricObject that, without stroke define a clipping area with their shape. filled in black - * the clipPath object gets used when the object has rendered, and the context is placed in the center - * of the object cacheCanvas. - * If you want 0,0 of a clipPath to align with an object center, use clipPath.originX/Y to 'center' - * @type fabric.Object - */ - clipPath: undefined, - - /** - * Meaningful ONLY when the object is used as clipPath. - * if true, the clipPath will make the object clip to the outside of the clipPath - * since 2.4.0 - * @type boolean - * @default false - */ - inverted: false, - - /** - * Meaningful ONLY when the object is used as clipPath. - * if true, the clipPath will have its top and left relative to canvas, and will - * not be influenced by the object transform. This will make the clipPath relative - * to the canvas, but clipping just a particular object. - * WARNING this is beta, this feature may change or be renamed. - * since 2.4.0 - * @type boolean - * @default false - */ - absolutePositioned: false, - - /** - * Constructor - * @param {Object} [options] Options object - */ - initialize: function (options) { - if (options) { - this.setOptions(options); - } - }, - - /** - * Create a the canvas used to keep the cached copy of the object - * @private - */ - _createCacheCanvas: function () { - this._cacheProperties = {}; - this._cacheCanvas = fabric.util.createCanvasElement(); - this._cacheContext = this._cacheCanvas.getContext('2d'); - this._updateCacheCanvas(); - // if canvas gets created, is empty, so dirty. - this.dirty = true; - }, - - /** - * Limit the cache dimensions so that X * Y do not cross fabric.perfLimitSizeTotal - * and each side do not cross fabric.cacheSideLimit - * those numbers are configurable so that you can get as much detail as you want - * making bargain with performances. - * @param {Object} dims - * @param {Object} dims.width width of canvas - * @param {Object} dims.height height of canvas - * @param {Object} dims.zoomX zoomX zoom value to unscale the canvas before drawing cache - * @param {Object} dims.zoomY zoomY zoom value to unscale the canvas before drawing cache - * @return {Object}.width width of canvas - * @return {Object}.height height of canvas - * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache - * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache - */ - _limitCacheSize: function (dims) { - var perfLimitSizeTotal = fabric.perfLimitSizeTotal, - width = dims.width, height = dims.height, - max = fabric.maxCacheSideLimit, min = fabric.minCacheSideLimit; - if (width <= max && height <= max && width * height <= perfLimitSizeTotal) { - if (width < min) { - dims.width = min; - } - if (height < min) { - dims.height = min; - } - return dims; - } - var ar = width / height, limitedDims = fabric.util.limitDimsByArea(ar, perfLimitSizeTotal), - capValue = fabric.util.capValue, - x = capValue(min, limitedDims.x, max), - y = capValue(min, limitedDims.y, max); - if (width > x) { - dims.zoomX /= width / x; - dims.width = x; - dims.capped = true; - } - if (height > y) { - dims.zoomY /= height / y; - dims.height = y; - dims.capped = true; - } - return dims; - }, - - /** - * Return the dimension and the zoom level needed to create a cache canvas - * big enough to host the object to be cached. - * @private - * @return {Object}.x width of object to be cached - * @return {Object}.y height of object to be cached - * @return {Object}.width width of canvas - * @return {Object}.height height of canvas - * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache - * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache - */ - _getCacheCanvasDimensions: function () { - var objectScale = this.getTotalObjectScaling(), - // caculate dimensions without skewing - dim = this._getTransformedDimensions(0, 0), - neededX = dim.x * objectScale.scaleX / this.scaleX, - neededY = dim.y * objectScale.scaleY / this.scaleY; - return { - // for sure this ALIASING_LIMIT is slightly creating problem - // in situation in which the cache canvas gets an upper limit - // also objectScale contains already scaleX and scaleY - width: neededX + ALIASING_LIMIT, - height: neededY + ALIASING_LIMIT, - zoomX: objectScale.scaleX, - zoomY: objectScale.scaleY, - x: neededX, - y: neededY - }; - }, - - /** - * Update width and height of the canvas for cache - * returns true or false if canvas needed resize. - * @private - * @return {Boolean} true if the canvas has been resized - */ - _updateCacheCanvas: function () { - var targetCanvas = this.canvas; - if (this.noScaleCache && targetCanvas && targetCanvas._currentTransform) { - var target = targetCanvas._currentTransform.target, - action = targetCanvas._currentTransform.action; - if (this === target && action.slice && action.slice(0, 5) === 'scale') { - return false; - } - } - var canvas = this._cacheCanvas, - dims = this._limitCacheSize(this._getCacheCanvasDimensions()), - minCacheSize = fabric.minCacheSideLimit, - width = dims.width, height = dims.height, drawingWidth, drawingHeight, - zoomX = dims.zoomX, zoomY = dims.zoomY, - dimensionsChanged = width !== this.cacheWidth || height !== this.cacheHeight, - zoomChanged = this.zoomX !== zoomX || this.zoomY !== zoomY, - shouldRedraw = dimensionsChanged || zoomChanged, - additionalWidth = 0, additionalHeight = 0, shouldResizeCanvas = false; - if (dimensionsChanged) { - var canvasWidth = this._cacheCanvas.width, - canvasHeight = this._cacheCanvas.height, - sizeGrowing = width > canvasWidth || height > canvasHeight, - sizeShrinking = (width < canvasWidth * 0.9 || height < canvasHeight * 0.9) && - canvasWidth > minCacheSize && canvasHeight > minCacheSize; - shouldResizeCanvas = sizeGrowing || sizeShrinking; - if (sizeGrowing && !dims.capped && (width > minCacheSize || height > minCacheSize)) { - additionalWidth = width * 0.1; - additionalHeight = height * 0.1; - } - } - if (this instanceof fabric.Text && this.path) { - shouldRedraw = true; - shouldResizeCanvas = true; - additionalWidth += this.getHeightOfLine(0) * this.zoomX; - additionalHeight += this.getHeightOfLine(0) * this.zoomY; - } - if (shouldRedraw) { - if (shouldResizeCanvas) { - canvas.width = Math.ceil(width + additionalWidth); - canvas.height = Math.ceil(height + additionalHeight); - } - else { - this._cacheContext.setTransform(1, 0, 0, 1, 0, 0); - this._cacheContext.clearRect(0, 0, canvas.width, canvas.height); - } - drawingWidth = dims.x / 2; - drawingHeight = dims.y / 2; - this.cacheTranslationX = Math.round(canvas.width / 2 - drawingWidth) + drawingWidth; - this.cacheTranslationY = Math.round(canvas.height / 2 - drawingHeight) + drawingHeight; - this.cacheWidth = width; - this.cacheHeight = height; - this._cacheContext.translate(this.cacheTranslationX, this.cacheTranslationY); - this._cacheContext.scale(zoomX, zoomY); - this.zoomX = zoomX; - this.zoomY = zoomY; - return true; - } - return false; - }, - - /** - * Sets object's properties from options - * @param {Object} [options] Options object - */ - setOptions: function (options) { - this._setOptions(options); - this._initGradient(options.fill, 'fill'); - this._initGradient(options.stroke, 'stroke'); - this._initPattern(options.fill, 'fill'); - this._initPattern(options.stroke, 'stroke'); - }, - - /** - * Transforms context when rendering an object - * @param {CanvasRenderingContext2D} ctx Context - */ - transform: function (ctx) { - var needFullTransform = (this.group && !this.group._transformDone) || - (this.group && this.canvas && ctx === this.canvas.contextTop); - var m = this.calcTransformMatrix(!needFullTransform); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - }, - - /** - * Returns an object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of an instance - */ - toObject: function (propertiesToInclude) { - var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, - - object = { - type: this.type, - version: fabric.version, - originX: this.originX, - originY: this.originY, - left: toFixed(this.left, NUM_FRACTION_DIGITS), - top: toFixed(this.top, NUM_FRACTION_DIGITS), - width: toFixed(this.width, NUM_FRACTION_DIGITS), - height: toFixed(this.height, NUM_FRACTION_DIGITS), - fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, - stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke, - strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), - strokeDashArray: this.strokeDashArray ? this.strokeDashArray.concat() : this.strokeDashArray, - strokeLineCap: this.strokeLineCap, - strokeDashOffset: this.strokeDashOffset, - strokeLineJoin: this.strokeLineJoin, - strokeUniform: this.strokeUniform, - strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS), - scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), - scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), - angle: toFixed(this.angle, NUM_FRACTION_DIGITS), - flipX: this.flipX, - flipY: this.flipY, - opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), - shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow, - visible: this.visible, - backgroundColor: this.backgroundColor, - fillRule: this.fillRule, - paintFirst: this.paintFirst, - globalCompositeOperation: this.globalCompositeOperation, - skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS), - skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS), - }; - - if (this.clipPath && !this.clipPath.excludeFromExport) { - object.clipPath = this.clipPath.toObject(propertiesToInclude); - object.clipPath.inverted = this.clipPath.inverted; - object.clipPath.absolutePositioned = this.clipPath.absolutePositioned; - } - - fabric.util.populateWithProperties(this, object, propertiesToInclude); - if (!this.includeDefaultValues) { - object = this._removeDefaultValues(object); - } - - return object; - }, - - /** - * Returns (dataless) object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of an instance - */ - toDatalessObject: function (propertiesToInclude) { - // will be overwritten by subclasses - return this.toObject(propertiesToInclude); - }, - - /** - * @private - * @param {Object} object - */ - _removeDefaultValues: function (object) { - var prototype = fabric.util.getKlass(object.type).prototype, - stateProperties = prototype.stateProperties; - stateProperties.forEach(function (prop) { - if (prop === 'left' || prop === 'top') { - return; - } - if (object[prop] === prototype[prop]) { - delete object[prop]; - } - // basically a check for [] === [] - if (Array.isArray(object[prop]) && Array.isArray(prototype[prop]) - && object[prop].length === 0 && prototype[prop].length === 0) { - delete object[prop]; - } - }); - - return object; - }, - - /** - * Returns a string representation of an instance - * @return {String} - */ - toString: function () { - return '#'; - }, - - /** - * Return the object scale factor counting also the group scaling - * @return {Object} object with scaleX and scaleY properties - */ - getObjectScaling: function () { - // if the object is a top level one, on the canvas, we go for simple aritmetic - // otherwise the complex method with angles will return approximations and decimals - // and will likely kill the cache when not needed - // https://github.com/fabricjs/fabric.js/issues/7157 - if (!this.group) { - return { - scaleX: this.scaleX, - scaleY: this.scaleY, - }; - } - // if we are inside a group total zoom calculation is complex, we defer to generic matrices - var options = fabric.util.qrDecompose(this.calcTransformMatrix()); - return { scaleX: Math.abs(options.scaleX), scaleY: Math.abs(options.scaleY) }; - }, - - /** - * Return the object scale factor counting also the group scaling, zoom and retina - * @return {Object} object with scaleX and scaleY properties - */ - getTotalObjectScaling: function () { - var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; - if (this.canvas) { - var zoom = this.canvas.getZoom(); - var retina = this.canvas.getRetinaScaling(); - scaleX *= zoom * retina; - scaleY *= zoom * retina; - } - return { scaleX: scaleX, scaleY: scaleY }; - }, - - /** - * Return the object opacity counting also the group property - * @return {Number} - */ - getObjectOpacity: function () { - var opacity = this.opacity; - if (this.group) { - opacity *= this.group.getObjectOpacity(); - } - return opacity; - }, - - /** - * @private - * @param {String} key - * @param {*} value - * @return {fabric.Object} thisArg - */ - _set: function (key, value) { - var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY'), - isChanged = this[key] !== value, groupNeedsUpdate = false; - - if (shouldConstrainValue) { - value = this._constrainScale(value); - } - if (key === 'scaleX' && value < 0) { - this.flipX = !this.flipX; - value *= -1; - } - else if (key === 'scaleY' && value < 0) { - this.flipY = !this.flipY; - value *= -1; - } - else if (key === 'shadow' && value && !(value instanceof fabric.Shadow)) { - value = new fabric.Shadow(value); - } - else if (key === 'dirty' && this.group) { - this.group.set('dirty', value); - } - - this[key] = value; - - if (isChanged) { - groupNeedsUpdate = this.group && this.group.isOnACache(); - if (this.cacheProperties.indexOf(key) > -1) { - this.dirty = true; - groupNeedsUpdate && this.group.set('dirty', true); - } - else if (groupNeedsUpdate && this.stateProperties.indexOf(key) > -1) { - this.group.set('dirty', true); - } - } - return this; - }, - - /** - * This callback function is called by the parent group of an object every - * time a non-delegated property changes on the group. It is passed the key - * and value as parameters. Not adding in this function's signature to avoid - * Travis build error about unused variables. - */ - setOnGroup: function () { - // implemented by sub-classes, as needed. - }, - - /** - * Retrieves viewportTransform from Object's canvas if possible - * @method getViewportTransform - * @memberOf fabric.Object.prototype - * @return {Array} - */ - getViewportTransform: function () { - if (this.canvas && this.canvas.viewportTransform) { - return this.canvas.viewportTransform; - } - return fabric.iMatrix.concat(); - }, - - /* - * @private - * return if the object would be visible in rendering - * @memberOf fabric.Object.prototype - * @return {Boolean} - */ - isNotVisible: function () { - return this.opacity === 0 || - (!this.width && !this.height && this.strokeWidth === 0) || - !this.visible; - }, - - /** - * Renders an object on a specified context - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - render: function (ctx) { - // do not render if width/height are zeros or object is not visible - if (this.isNotVisible()) { - return; - } - if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { - return; - } - ctx.save(); - this._setupCompositeOperation(ctx); - this.drawSelectionBackground(ctx); - this.transform(ctx); - this._setOpacity(ctx); - this._setShadow(ctx, this); - if (this.shouldCache()) { - this.renderCache(); - this.drawCacheOnCanvas(ctx); - } - else { - this._removeCacheCanvas(); - this.dirty = false; - this.drawObject(ctx); - if (this.objectCaching && this.statefullCache) { - this.saveState({ propertySet: 'cacheProperties' }); - } - } - ctx.restore(); - }, - - renderCache: function (options) { - options = options || {}; - if (!this._cacheCanvas || !this._cacheContext) { - this._createCacheCanvas(); - } - if (this.isCacheDirty()) { - this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); - this.drawObject(this._cacheContext, options.forClipping); - this.dirty = false; - } - }, - - /** - * Remove cacheCanvas and its dimensions from the objects - */ - _removeCacheCanvas: function () { - this._cacheCanvas = null; - this._cacheContext = null; - this.cacheWidth = 0; - this.cacheHeight = 0; - }, - - /** - * return true if the object will draw a stroke - * Does not consider text styles. This is just a shortcut used at rendering time - * We want it to be an approximation and be fast. - * wrote to avoid extra caching, it has to return true when stroke happens, - * can guess when it will not happen at 100% chance, does not matter if it misses - * some use case where the stroke is invisible. - * @since 3.0.0 - * @returns Boolean - */ - hasStroke: function () { - return this.stroke && this.stroke !== 'transparent' && this.strokeWidth !== 0; - }, - - /** - * return true if the object will draw a fill - * Does not consider text styles. This is just a shortcut used at rendering time - * We want it to be an approximation and be fast. - * wrote to avoid extra caching, it has to return true when fill happens, - * can guess when it will not happen at 100% chance, does not matter if it misses - * some use case where the fill is invisible. - * @since 3.0.0 - * @returns Boolean - */ - hasFill: function () { - return this.fill && this.fill !== 'transparent'; - }, - - /** - * When set to `true`, force the object to have its own cache, even if it is inside a group - * it may be needed when your object behave in a particular way on the cache and always needs - * its own isolated canvas to render correctly. - * Created to be overridden - * since 1.7.12 - * @returns Boolean - */ - needsItsOwnCache: function () { - if (this.paintFirst === 'stroke' && - this.hasFill() && this.hasStroke() && typeof this.shadow === 'object') { - return true; - } - if (this.clipPath) { - return true; - } - return false; - }, - - /** - * Decide if the object should cache or not. Create its own cache level - * objectCaching is a global flag, wins over everything - * needsItsOwnCache should be used when the object drawing method requires - * a cache step. None of the fabric classes requires it. - * Generally you do not cache objects in groups because the group outside is cached. - * Read as: cache if is needed, or if the feature is enabled but we are not already caching. - * @return {Boolean} - */ - shouldCache: function () { - this.ownCaching = this.needsItsOwnCache() || ( - this.objectCaching && - (!this.group || !this.group.isOnACache()) - ); - return this.ownCaching; - }, - - /** - * Check if this object or a child object will cast a shadow - * used by Group.shouldCache to know if child has a shadow recursively - * @return {Boolean} - */ - willDrawShadow: function () { - return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); - }, - - /** - * Execute the drawing operation for an object clipPath - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {fabric.Object} clipPath - */ - drawClipPathOnCache: function (ctx, clipPath) { - ctx.save(); - // DEBUG: uncomment this line, comment the following - // ctx.globalAlpha = 0.4 - if (clipPath.inverted) { - ctx.globalCompositeOperation = 'destination-out'; - } - else { - ctx.globalCompositeOperation = 'destination-in'; - } - //ctx.scale(1 / 2, 1 / 2); - if (clipPath.absolutePositioned) { - var m = fabric.util.invertTransform(this.calcTransformMatrix()); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - clipPath.transform(ctx); - ctx.scale(1 / clipPath.zoomX, 1 / clipPath.zoomY); - ctx.drawImage(clipPath._cacheCanvas, -clipPath.cacheTranslationX, -clipPath.cacheTranslationY); - ctx.restore(); - }, - - /** - * Execute the drawing operation for an object on a specified context - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - drawObject: function (ctx, forClipping) { - var originalFill = this.fill, originalStroke = this.stroke; - if (forClipping) { - this.fill = 'black'; - this.stroke = ''; - this._setClippingProperties(ctx); - } - else { - this._renderBackground(ctx); - } - this._render(ctx); - this._drawClipPath(ctx, this.clipPath); - this.fill = originalFill; - this.stroke = originalStroke; - }, - - /** - * Prepare clipPath state and cache and draw it on instance's cache - * @param {CanvasRenderingContext2D} ctx - * @param {fabric.Object} clipPath - */ - _drawClipPath: function (ctx, clipPath) { - if (!clipPath) { return; } - // needed to setup a couple of variables - // path canvas gets overridden with this one. - // TODO find a better solution? - clipPath.canvas = this.canvas; - clipPath.shouldCache(); - clipPath._transformDone = true; - clipPath.renderCache({ forClipping: true }); - this.drawClipPathOnCache(ctx, clipPath); - }, - - /** - * Paint the cached copy of the object on the target context. - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - drawCacheOnCanvas: function (ctx) { - ctx.scale(1 / this.zoomX, 1 / this.zoomY); - ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY); - }, - - /** - * Check if cache is dirty - * @param {Boolean} skipCanvas skip canvas checks because this object is painted - * on parent canvas. - */ - isCacheDirty: function (skipCanvas) { - if (this.isNotVisible()) { - return false; - } - if (this._cacheCanvas && this._cacheContext && !skipCanvas && this._updateCacheCanvas()) { - // in this case the context is already cleared. - return true; - } - else { - if (this.dirty || - (this.clipPath && this.clipPath.absolutePositioned) || - (this.statefullCache && this.hasStateChanged('cacheProperties')) - ) { - if (this._cacheCanvas && this._cacheContext && !skipCanvas) { - var width = this.cacheWidth / this.zoomX; - var height = this.cacheHeight / this.zoomY; - this._cacheContext.clearRect(-width / 2, -height / 2, width, height); - } - return true; - } - } - return false; - }, - - /** - * Draws a background for the object big as its untransformed dimensions - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderBackground: function (ctx) { - if (!this.backgroundColor) { - return; - } - var dim = this._getNonTransformedDimensions(); - ctx.fillStyle = this.backgroundColor; - - ctx.fillRect( - -dim.x / 2, - -dim.y / 2, - dim.x, - dim.y - ); - // if there is background color no other shadows - // should be casted - this._removeShadow(ctx); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _setOpacity: function (ctx) { - if (this.group && !this.group._transformDone) { - ctx.globalAlpha = this.getObjectOpacity(); - } - else { - ctx.globalAlpha *= this.opacity; - } - }, - - _setStrokeStyles: function (ctx, decl) { - var stroke = decl.stroke; - if (stroke) { - ctx.lineWidth = decl.strokeWidth; - ctx.lineCap = decl.strokeLineCap; - ctx.lineDashOffset = decl.strokeDashOffset; - ctx.lineJoin = decl.strokeLineJoin; - ctx.miterLimit = decl.strokeMiterLimit; - if (stroke.toLive) { - if (stroke.gradientUnits === 'percentage' || stroke.gradientTransform || stroke.patternTransform) { - // need to transform gradient in a pattern. - // this is a slow process. If you are hitting this codepath, and the object - // is not using caching, you should consider switching it on. - // we need a canvas as big as the current object caching canvas. - this._applyPatternForTransformedGradient(ctx, stroke); - } - else { - // is a simple gradient or pattern - ctx.strokeStyle = stroke.toLive(ctx, this); - this._applyPatternGradientTransform(ctx, stroke); - } - } - else { - // is a color - ctx.strokeStyle = decl.stroke; - } - } - }, - - _setFillStyles: function (ctx, decl) { - var fill = decl.fill; - if (fill) { - if (fill.toLive) { - ctx.fillStyle = fill.toLive(ctx, this); - this._applyPatternGradientTransform(ctx, decl.fill); - } - else { - ctx.fillStyle = fill; - } - } - }, - - _setClippingProperties: function (ctx) { - ctx.globalAlpha = 1; - ctx.strokeStyle = 'transparent'; - ctx.fillStyle = '#000000'; - }, - - /** - * @private - * Sets line dash - * @param {CanvasRenderingContext2D} ctx Context to set the dash line on - * @param {Array} dashArray array representing dashes - */ - _setLineDash: function (ctx, dashArray) { - if (!dashArray || dashArray.length === 0) { - return; - } - // Spec requires the concatenation of two copies the dash list when the number of elements is odd - if (1 & dashArray.length) { - dashArray.push.apply(dashArray, dashArray); - } - ctx.setLineDash(dashArray); - }, - - /** - * Renders controls and borders for the object - * the context here is not transformed - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Object} [styleOverride] properties to override the object style - */ - _renderControls: function (ctx, styleOverride) { - var vpt = this.getViewportTransform(), - matrix = this.calcTransformMatrix(), - options, drawBorders, drawControls; - styleOverride = styleOverride || {}; - drawBorders = typeof styleOverride.hasBorders !== 'undefined' ? styleOverride.hasBorders : this.hasBorders; - drawControls = typeof styleOverride.hasControls !== 'undefined' ? styleOverride.hasControls : this.hasControls; - matrix = fabric.util.multiplyTransformMatrices(vpt, matrix); - options = fabric.util.qrDecompose(matrix); - ctx.save(); - ctx.translate(options.translateX, options.translateY); - ctx.lineWidth = 1 * this.borderScaleFactor; - if (!this.group) { - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - } - if (this.flipX) { - options.angle -= 180; - } - ctx.rotate(degreesToRadians(this.group ? options.angle : this.angle)); - if (styleOverride.forActiveSelection || this.group) { - drawBorders && this.drawBordersInGroup(ctx, options, styleOverride); - } - else { - drawBorders && this.drawBorders(ctx, styleOverride); - } - drawControls && this.drawControls(ctx, styleOverride); - ctx.restore(); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _setShadow: function (ctx) { - if (!this.shadow) { - return; - } - - var shadow = this.shadow, canvas = this.canvas, scaling, - multX = (canvas && canvas.viewportTransform[0]) || 1, - multY = (canvas && canvas.viewportTransform[3]) || 1; - if (shadow.nonScaling) { - scaling = { scaleX: 1, scaleY: 1 }; - } - else { - scaling = this.getObjectScaling(); - } - if (canvas && canvas._isRetinaScaling()) { - multX *= fabric.devicePixelRatio; - multY *= fabric.devicePixelRatio; - } - ctx.shadowColor = shadow.color; - ctx.shadowBlur = shadow.blur * fabric.browserShadowBlurConstant * - (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; - ctx.shadowOffsetX = shadow.offsetX * multX * scaling.scaleX; - ctx.shadowOffsetY = shadow.offsetY * multY * scaling.scaleY; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _removeShadow: function (ctx) { - if (!this.shadow) { - return; - } - - ctx.shadowColor = ''; - ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Object} filler fabric.Pattern or fabric.Gradient - * @return {Object} offset.offsetX offset for text rendering - * @return {Object} offset.offsetY offset for text rendering - */ - _applyPatternGradientTransform: function (ctx, filler) { - if (!filler || !filler.toLive) { - return { offsetX: 0, offsetY: 0 }; - } - var t = filler.gradientTransform || filler.patternTransform; - var offsetX = -this.width / 2 + filler.offsetX || 0, - offsetY = -this.height / 2 + filler.offsetY || 0; - - if (filler.gradientUnits === 'percentage') { - ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY); - } - else { - ctx.transform(1, 0, 0, 1, offsetX, offsetY); - } - if (t) { - ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]); - } - return { offsetX: offsetX, offsetY: offsetY }; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderPaintInOrder: function (ctx) { - if (this.paintFirst === 'stroke') { - this._renderStroke(ctx); - this._renderFill(ctx); - } - else { - this._renderFill(ctx); - this._renderStroke(ctx); - } - }, - - /** - * @private - * function that actually render something on the context. - * empty here to allow Obects to work on tests to benchmark fabric functionalites - * not related to rendering - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (/* ctx */) { - - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderFill: function (ctx) { - if (!this.fill) { - return; - } - - ctx.save(); - this._setFillStyles(ctx, this); - if (this.fillRule === 'evenodd') { - ctx.fill('evenodd'); - } - else { - ctx.fill(); - } - ctx.restore(); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderStroke: function (ctx) { - if (!this.stroke || this.strokeWidth === 0) { - return; - } - - if (this.shadow && !this.shadow.affectStroke) { - this._removeShadow(ctx); - } - - ctx.save(); - if (this.strokeUniform && this.group) { - var scaling = this.getObjectScaling(); - ctx.scale(1 / scaling.scaleX, 1 / scaling.scaleY); - } - else if (this.strokeUniform) { - ctx.scale(1 / this.scaleX, 1 / this.scaleY); - } - this._setLineDash(ctx, this.strokeDashArray); - this._setStrokeStyles(ctx, this); - ctx.stroke(); - ctx.restore(); - }, - - /** - * This function try to patch the missing gradientTransform on canvas gradients. - * transforming a context to transform the gradient, is going to transform the stroke too. - * we want to transform the gradient but not the stroke operation, so we create - * a transformed gradient on a pattern and then we use the pattern instead of the gradient. - * this method has drwabacks: is slow, is in low resolution, needs a patch for when the size - * is limited. - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {fabric.Gradient} filler a fabric gradient instance - */ - _applyPatternForTransformedGradient: function (ctx, filler) { - var dims = this._limitCacheSize(this._getCacheCanvasDimensions()), - pCanvas = fabric.util.createCanvasElement(), pCtx, retinaScaling = this.canvas.getRetinaScaling(), - width = dims.x / this.scaleX / retinaScaling, height = dims.y / this.scaleY / retinaScaling; - pCanvas.width = width; - pCanvas.height = height; - pCtx = pCanvas.getContext('2d'); - pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); - pCtx.lineTo(0, height); pCtx.closePath(); - pCtx.translate(width / 2, height / 2); - pCtx.scale( - dims.zoomX / this.scaleX / retinaScaling, - dims.zoomY / this.scaleY / retinaScaling - ); - this._applyPatternGradientTransform(pCtx, filler); - pCtx.fillStyle = filler.toLive(ctx); - pCtx.fill(); - ctx.translate(-this.width / 2 - this.strokeWidth / 2, -this.height / 2 - this.strokeWidth / 2); - ctx.scale( - retinaScaling * this.scaleX / dims.zoomX, - retinaScaling * this.scaleY / dims.zoomY - ); - ctx.strokeStyle = pCtx.createPattern(pCanvas, 'no-repeat'); - }, - - /** - * This function is an helper for svg import. it returns the center of the object in the svg - * untransformed coordinates - * @private - * @return {Object} center point from element coordinates - */ - _findCenterFromElement: function () { - return { x: this.left + this.width / 2, y: this.top + this.height / 2 }; - }, - - /** - * This function is an helper for svg import. it decompose the transformMatrix - * and assign properties to object. - * untransformed coordinates - * @private - * @chainable - */ - _assignTransformMatrixProps: function () { - if (this.transformMatrix) { - var options = fabric.util.qrDecompose(this.transformMatrix); - this.flipX = false; - this.flipY = false; - this.set('scaleX', options.scaleX); - this.set('scaleY', options.scaleY); - this.angle = options.angle; - this.skewX = options.skewX; - this.skewY = 0; - } - }, - - /** - * This function is an helper for svg import. it removes the transform matrix - * and set to object properties that fabricjs can handle - * @private - * @param {Object} preserveAspectRatioOptions - * @return {thisArg} - */ - _removeTransformMatrix: function (preserveAspectRatioOptions) { - var center = this._findCenterFromElement(); - if (this.transformMatrix) { - this._assignTransformMatrixProps(); - center = fabric.util.transformPoint(center, this.transformMatrix); - } - this.transformMatrix = null; - if (preserveAspectRatioOptions) { - this.scaleX *= preserveAspectRatioOptions.scaleX; - this.scaleY *= preserveAspectRatioOptions.scaleY; - this.cropX = preserveAspectRatioOptions.cropX; - this.cropY = preserveAspectRatioOptions.cropY; - center.x += preserveAspectRatioOptions.offsetLeft; - center.y += preserveAspectRatioOptions.offsetTop; - this.width = preserveAspectRatioOptions.width; - this.height = preserveAspectRatioOptions.height; - } - this.setPositionByOrigin(center, 'center', 'center'); - }, - - /** - * Clones an instance, using a callback method will work for every object. - * @param {Function} callback Callback is invoked with a clone as a first argument - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - */ - clone: function (callback, propertiesToInclude) { - var objectForm = this.toObject(propertiesToInclude); - if (this.constructor.fromObject) { - this.constructor.fromObject(objectForm, callback); - } - else { - fabric.Object._fromObject('Object', objectForm, callback); - } - }, - - /** - * Creates an instance of fabric.Image out of an object - * makes use of toCanvasElement. - * Once this method was based on toDataUrl and loadImage, so it also had a quality - * and format option. toCanvasElement is faster and produce no loss of quality. - * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. - * toCanvasElement and then toBlob from the obtained canvas is also a good option. - * This method is sync now, but still support the callback because we did not want to break. - * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback. - * @param {Function} callback callback, invoked with an instance as a first argument - * @param {Object} [options] for clone as image, passed to toDataURL - * @param {Number} [options.multiplier=1] Multiplier to scale by - * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 - * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 - * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 - * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 - * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 - * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 - * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 - * @return {fabric.Object} thisArg - */ - cloneAsImage: function (callback, options) { - var canvasEl = this.toCanvasElement(options); - if (callback) { - callback(new fabric.Image(canvasEl)); - } - return this; - }, - - /** - * Converts an object into a HTMLCanvas element - * @param {Object} options Options object - * @param {Number} [options.multiplier=1] Multiplier to scale by - * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 - * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 - * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 - * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 - * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 - * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 - * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 - * @return {HTMLCanvasElement} Returns DOM element with the fabric.Object - */ - toCanvasElement: function (options) { - options || (options = {}); - - var utils = fabric.util, origParams = utils.saveObjectTransform(this), - originalGroup = this.group, - originalShadow = this.shadow, abs = Math.abs, - multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? fabric.devicePixelRatio : 1); - delete this.group; - if (options.withoutTransform) { - utils.resetObjectTransform(this); - } - if (options.withoutShadow) { - this.shadow = null; - } - - var el = fabric.util.createCanvasElement(), - // skip canvas zoom and calculate with setCoords now. - boundingRect = this.getBoundingRect(true, true), - shadow = this.shadow, scaling, - shadowOffset = { x: 0, y: 0 }, shadowBlur, - width, height; - - if (shadow) { - shadowBlur = shadow.blur; - if (shadow.nonScaling) { - scaling = { scaleX: 1, scaleY: 1 }; - } - else { - scaling = this.getObjectScaling(); - } - // consider non scaling shadow. - shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); - shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); - } - width = boundingRect.width + shadowOffset.x; - height = boundingRect.height + shadowOffset.y; - // if the current width/height is not an integer - // we need to make it so. - el.width = Math.ceil(width); - el.height = Math.ceil(height); - var canvas = new fabric.StaticCanvas(el, { - enableRetinaScaling: false, - renderOnAddRemove: false, - skipOffscreen: false, - }); - if (options.format === 'jpeg') { - canvas.backgroundColor = '#fff'; - } - this.setPositionByOrigin(new fabric.Point(canvas.width / 2, canvas.height / 2), 'center', 'center'); - - var originalCanvas = this.canvas; - canvas.add(this); - var canvasEl = canvas.toCanvasElement(multiplier || 1, options); - this.shadow = originalShadow; - this.set('canvas', originalCanvas); - if (originalGroup) { - this.group = originalGroup; - } - this.set(origParams).setCoords(); - // canvas.dispose will call image.dispose that will nullify the elements - // since this canvas is a simple element for the process, we remove references - // to objects in this way in order to avoid object trashing. - canvas._objects = []; - canvas.dispose(); - canvas = null; - - return canvasEl; - }, - - /** - * Converts an object into a data-url-like string - * @param {Object} options Options object - * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" - * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. - * @param {Number} [options.multiplier=1] Multiplier to scale by - * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 - * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 - * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 - * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 - * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 - * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 - * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 - * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format - */ - toDataURL: function (options) { - options || (options = {}); - return fabric.util.toDataURL(this.toCanvasElement(options), options.format || 'png', options.quality || 1); - }, - - /** - * Returns true if specified type is identical to the type of an instance - * @param {String} type Type to check against - * @return {Boolean} - */ - isType: function (type) { - return arguments.length > 1 ? Array.from(arguments).includes(this.type) : this.type === type; - }, - - /** - * Returns complexity of an instance - * @return {Number} complexity of this instance (is 1 unless subclassed) - */ - complexity: function () { - return 1; - }, - - /** - * Returns a JSON representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} JSON - */ - toJSON: function (propertiesToInclude) { - // delegate, not alias - return this.toObject(propertiesToInclude); - }, - - /** - * Sets "angle" of an instance with centered rotation - * @param {Number} angle Angle value (in degrees) - * @return {fabric.Object} thisArg - * @chainable - */ - rotate: function (angle) { - var shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation; - - if (shouldCenterOrigin) { - this._setOriginToCenter(); - } - - this.set('angle', angle); - - if (shouldCenterOrigin) { - this._resetOrigin(); - } - - return this; - }, - - /** - * Centers object horizontally on canvas to which it was added last. - * You might need to call `setCoords` on an object after centering, to update controls area. - * @return {fabric.Object} thisArg - * @chainable - */ - centerH: function () { - this.canvas && this.canvas.centerObjectH(this); - return this; - }, - - /** - * Centers object horizontally on current viewport of canvas to which it was added last. - * You might need to call `setCoords` on an object after centering, to update controls area. - * @return {fabric.Object} thisArg - * @chainable - */ - viewportCenterH: function () { - this.canvas && this.canvas.viewportCenterObjectH(this); - return this; - }, - - /** - * Centers object vertically on canvas to which it was added last. - * You might need to call `setCoords` on an object after centering, to update controls area. - * @return {fabric.Object} thisArg - * @chainable - */ - centerV: function () { - this.canvas && this.canvas.centerObjectV(this); - return this; - }, - - /** - * Centers object vertically on current viewport of canvas to which it was added last. - * You might need to call `setCoords` on an object after centering, to update controls area. - * @return {fabric.Object} thisArg - * @chainable - */ - viewportCenterV: function () { - this.canvas && this.canvas.viewportCenterObjectV(this); - return this; - }, - - /** - * Centers object vertically and horizontally on canvas to which is was added last - * You might need to call `setCoords` on an object after centering, to update controls area. - * @return {fabric.Object} thisArg - * @chainable - */ - center: function () { - this.canvas && this.canvas.centerObject(this); - return this; - }, - - /** - * Centers object on current viewport of canvas to which it was added last. - * You might need to call `setCoords` on an object after centering, to update controls area. - * @return {fabric.Object} thisArg - * @chainable - */ - viewportCenter: function () { - this.canvas && this.canvas.viewportCenterObject(this); - return this; - }, - - /** - * Returns coordinates of a pointer relative to an object - * @param {Event} e Event to operate upon - * @param {Object} [pointer] Pointer to operate upon (instead of event) - * @return {Object} Coordinates of a pointer (x, y) - */ - getLocalPointer: function (e, pointer) { - pointer = pointer || this.canvas.getPointer(e); - var pClicked = new fabric.Point(pointer.x, pointer.y), - objectLeftTop = this._getLeftTopCoords(); - if (this.angle) { - pClicked = fabric.util.rotatePoint( - pClicked, objectLeftTop, degreesToRadians(-this.angle)); - } - return { - x: pClicked.x - objectLeftTop.x, - y: pClicked.y - objectLeftTop.y - }; - }, - - /** - * Sets canvas globalCompositeOperation for specific object - * custom composition operation for the particular object can be specified using globalCompositeOperation property - * @param {CanvasRenderingContext2D} ctx Rendering canvas context - */ - _setupCompositeOperation: function (ctx) { - if (this.globalCompositeOperation) { - ctx.globalCompositeOperation = this.globalCompositeOperation; - } - }, - - /** - * cancel instance's running animations - * override if necessary to dispose artifacts such as `clipPath` - */ - dispose: function () { - if (fabric.runningAnimations) { - fabric.runningAnimations.cancelByTarget(this); - } - } - }); - - fabric.util.createAccessors && fabric.util.createAccessors(fabric.Object); - - extend(fabric.Object.prototype, fabric.Observable); - - /** - * Defines the number of fraction digits to use when serializing object values. - * You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc. - * @static - * @memberOf fabric.Object - * @constant - * @type Number - */ - fabric.Object.NUM_FRACTION_DIGITS = 2; - - /** - * Defines which properties should be enlivened from the object passed to {@link fabric.Object._fromObject} - * @static - * @memberOf fabric.Object - * @constant - * @type string[] - */ - fabric.Object.ENLIVEN_PROPS = ['clipPath']; - - fabric.Object._fromObject = function (className, object, callback, extraParam) { - var klass = fabric[className]; - object = clone(object, true); - fabric.util.enlivenPatterns([object.fill, object.stroke], function (patterns) { - if (typeof patterns[0] !== 'undefined') { - object.fill = patterns[0]; - } - if (typeof patterns[1] !== 'undefined') { - object.stroke = patterns[1]; - } - fabric.util.enlivenObjectEnlivables(object, object, function () { - var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); - callback && callback(instance); - }); - }); - }; - - /** - * Unique id used internally when creating SVG elements - * @static - * @memberOf fabric.Object - * @type Number - */ - fabric.Object.__uid = 0; -})(typeof exports !== 'undefined' ? exports : this); - - -(function () { - - var degreesToRadians = fabric.util.degreesToRadians, - originXOffset = { - left: -0.5, - center: 0, - right: 0.5 - }, - originYOffset = { - top: -0.5, - center: 0, - bottom: 0.5 - }; - - fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - - /** - * Translates the coordinates from a set of origin to another (based on the object's dimensions) - * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {String} fromOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' - * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' - * @return {fabric.Point} - */ - translateToGivenOrigin: function (point, fromOriginX, fromOriginY, toOriginX, toOriginY) { - var x = point.x, - y = point.y, - offsetX, offsetY, dim; - - if (typeof fromOriginX === 'string') { - fromOriginX = originXOffset[fromOriginX]; - } - else { - fromOriginX -= 0.5; - } - - if (typeof toOriginX === 'string') { - toOriginX = originXOffset[toOriginX]; - } - else { - toOriginX -= 0.5; - } - - offsetX = toOriginX - fromOriginX; - - if (typeof fromOriginY === 'string') { - fromOriginY = originYOffset[fromOriginY]; - } - else { - fromOriginY -= 0.5; - } - - if (typeof toOriginY === 'string') { - toOriginY = originYOffset[toOriginY]; - } - else { - toOriginY -= 0.5; - } - - offsetY = toOriginY - fromOriginY; - - if (offsetX || offsetY) { - dim = this._getTransformedDimensions(); - x = point.x + offsetX * dim.x; - y = point.y + offsetY * dim.y; - } - - return new fabric.Point(x, y); - }, - - /** - * Translates the coordinates from origin to center coordinates (based on the object's dimensions) - * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {fabric.Point} - */ - translateToCenterPoint: function (point, originX, originY) { - var p = this.translateToGivenOrigin(point, originX, originY, 'center', 'center'); - if (this.angle) { - return fabric.util.rotatePoint(p, point, degreesToRadians(this.angle)); - } - return p; - }, - - /** - * Translates the coordinates from center to origin coordinates (based on the object's dimensions) - * @param {fabric.Point} center The point which corresponds to center of the object - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {fabric.Point} - */ - translateToOriginPoint: function (center, originX, originY) { - var p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); - if (this.angle) { - return fabric.util.rotatePoint(p, center, degreesToRadians(this.angle)); - } - return p; - }, - - /** - * Returns the real center coordinates of the object - * @return {fabric.Point} - */ - getCenterPoint: function () { - var leftTop = new fabric.Point(this.left, this.top); - return this.translateToCenterPoint(leftTop, this.originX, this.originY); - }, - - /** - * Returns the coordinates of the object based on center coordinates - * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @return {fabric.Point} - */ - // getOriginPoint: function(center) { - // return this.translateToOriginPoint(center, this.originX, this.originY); - // }, - - /** - * Returns the coordinates of the object as if it has a different origin - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {fabric.Point} - */ - getPointByOrigin: function (originX, originY) { - var center = this.getCenterPoint(); - return this.translateToOriginPoint(center, originX, originY); - }, - - /** - * Returns the point in local coordinates - * @param {fabric.Point} point The point relative to the global coordinate system - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {fabric.Point} - */ - toLocalPoint: function (point, originX, originY) { - var center = this.getCenterPoint(), - p, p2; - - if (typeof originX !== 'undefined' && typeof originY !== 'undefined') { - p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); - } - else { - p = new fabric.Point(this.left, this.top); - } - - p2 = new fabric.Point(point.x, point.y); - if (this.angle) { - p2 = fabric.util.rotatePoint(p2, center, -degreesToRadians(this.angle)); - } - return p2.subtractEquals(p); - }, - - /** - * Returns the point in global coordinates - * @param {fabric.Point} The point relative to the local coordinate system - * @return {fabric.Point} - */ - // toGlobalPoint: function(point) { - // return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new fabric.Point(this.left, this.top)); - // }, - - /** - * Sets the position of the object taking into consideration the object's origin - * @param {fabric.Point} pos The new position of the object - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {void} - */ - setPositionByOrigin: function (pos, originX, originY) { - var center = this.translateToCenterPoint(pos, originX, originY), - position = this.translateToOriginPoint(center, this.originX, this.originY); - this.set('left', position.x); - this.set('top', position.y); - }, - - /** - * @param {String} to One of 'left', 'center', 'right' - */ - adjustPosition: function (to) { - var angle = degreesToRadians(this.angle), - hypotFull = this.getScaledWidth(), - xFull = fabric.util.cos(angle) * hypotFull, - yFull = fabric.util.sin(angle) * hypotFull, - offsetFrom, offsetTo; - - //TODO: this function does not consider mixed situation like top, center. - if (typeof this.originX === 'string') { - offsetFrom = originXOffset[this.originX]; - } - else { - offsetFrom = this.originX - 0.5; - } - if (typeof to === 'string') { - offsetTo = originXOffset[to]; - } - else { - offsetTo = to - 0.5; - } - this.left += xFull * (offsetTo - offsetFrom); - this.top += yFull * (offsetTo - offsetFrom); - this.setCoords(); - this.originX = to; - }, - - /** - * Sets the origin/position of the object to it's center point - * @private - * @return {void} - */ - _setOriginToCenter: function () { - this._originalOriginX = this.originX; - this._originalOriginY = this.originY; - - var center = this.getCenterPoint(); - - this.originX = 'center'; - this.originY = 'center'; - - this.left = center.x; - this.top = center.y; - }, - - /** - * Resets the origin/position of the object to it's original origin - * @private - * @return {void} - */ - _resetOrigin: function () { - var originPoint = this.translateToOriginPoint( - this.getCenterPoint(), - this._originalOriginX, - this._originalOriginY); - - this.originX = this._originalOriginX; - this.originY = this._originalOriginY; - - this.left = originPoint.x; - this.top = originPoint.y; - - this._originalOriginX = null; - this._originalOriginY = null; - }, - - /** - * @private - */ - _getLeftTopCoords: function () { - return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); - }, - }); - -})(); - - -(function () { - - function arrayFromCoords(coords) { - return [ - new fabric.Point(coords.tl.x, coords.tl.y), - new fabric.Point(coords.tr.x, coords.tr.y), - new fabric.Point(coords.br.x, coords.br.y), - new fabric.Point(coords.bl.x, coords.bl.y) - ]; - } - - var util = fabric.util, - degreesToRadians = util.degreesToRadians, - multiplyMatrices = util.multiplyTransformMatrices, - transformPoint = util.transformPoint; - - util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - - /** - * Describe object's corner position in canvas element coordinates. - * properties are depending on control keys and padding the main controls. - * each property is an object with x, y and corner. - * The `corner` property contains in a similar manner the 4 points of the - * interactive area of the corner. - * The coordinates depends from the controls positionHandler and are used - * to draw and locate controls - * @memberOf fabric.Object.prototype - */ - oCoords: null, - - /** - * Describe object's corner position in canvas object absolute coordinates - * properties are tl,tr,bl,br and describe the four main corner. - * each property is an object with x, y, instance of Fabric.Point. - * The coordinates depends from this properties: width, height, scaleX, scaleY - * skewX, skewY, angle, strokeWidth, top, left. - * Those coordinates are useful to understand where an object is. They get updated - * with oCoords but they do not need to be updated when zoom or panning change. - * The coordinates get updated with @method setCoords. - * You can calculate them without updating with @method calcACoords(); - * @memberOf fabric.Object.prototype - */ - aCoords: null, - - /** - * Describe object's corner position in canvas element coordinates. - * includes padding. Used of object detection. - * set and refreshed with setCoords. - * @memberOf fabric.Object.prototype - */ - lineCoords: null, - - /** - * storage for object transform matrix - */ - ownMatrixCache: null, - - /** - * storage for object full transform matrix - */ - matrixCache: null, - - /** - * custom controls interface - * controls are added by default_controls.js - */ - controls: {}, - - /** - * return correct set of coordinates for intersection - * this will return either aCoords or lineCoords. - * @param {Boolean} absolute will return aCoords if true or lineCoords - * @return {Object} {tl, tr, br, bl} points - */ - _getCoords: function (absolute, calculate) { - if (calculate) { - return (absolute ? this.calcACoords() : this.calcLineCoords()); - } - if (!this.aCoords || !this.lineCoords) { - this.setCoords(true); - } - return (absolute ? this.aCoords : this.lineCoords); - }, - - /** - * return correct set of coordinates for intersection - * this will return either aCoords or lineCoords. - * The coords are returned in an array. - * @return {Array} [tl, tr, br, bl] of points - */ - getCoords: function (absolute, calculate) { - return arrayFromCoords(this._getCoords(absolute, calculate)); - }, - - /** - * Checks if object intersects with an area formed by 2 points - * @param {Object} pointTL top-left point of area - * @param {Object} pointBR bottom-right point of area - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object intersects with an area formed by 2 points - */ - intersectsWithRect: function (pointTL, pointBR, absolute, calculate) { - var coords = this.getCoords(absolute, calculate), - intersection = fabric.Intersection.intersectPolygonRectangle( - coords, - pointTL, - pointBR - ); - return intersection.status === 'Intersection'; - }, - - /** - * Checks if object intersects with another object - * @param {Object} other Object to test - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object intersects with another object - */ - intersectsWithObject: function (other, absolute, calculate) { - var intersection = fabric.Intersection.intersectPolygonPolygon( - this.getCoords(absolute, calculate), - other.getCoords(absolute, calculate) - ); - - return intersection.status === 'Intersection' - || other.isContainedWithinObject(this, absolute, calculate) - || this.isContainedWithinObject(other, absolute, calculate); - }, - - /** - * Checks if object is fully contained within area of another object - * @param {Object} other Object to test - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object is fully contained within area of another object - */ - isContainedWithinObject: function (other, absolute, calculate) { - var points = this.getCoords(absolute, calculate), - otherCoords = absolute ? other.aCoords : other.lineCoords, - i = 0, lines = other._getImageLines(otherCoords); - for (; i < 4; i++) { - if (!other.containsPoint(points[i], lines)) { - return false; - } - } - return true; - }, - - /** - * Checks if object is fully contained within area formed by 2 points - * @param {Object} pointTL top-left point of area - * @param {Object} pointBR bottom-right point of area - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object is fully contained within area formed by 2 points - */ - isContainedWithinRect: function (pointTL, pointBR, absolute, calculate) { - var boundingRect = this.getBoundingRect(absolute, calculate); - - return ( - boundingRect.left >= pointTL.x && - boundingRect.left + boundingRect.width <= pointBR.x && - boundingRect.top >= pointTL.y && - boundingRect.top + boundingRect.height <= pointBR.y - ); - }, - - /** - * Checks if point is inside the object - * @param {fabric.Point} point Point to check against - * @param {Object} [lines] object returned from @method _getImageLines - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if point is inside the object - */ - containsPoint: function (point, lines, absolute, calculate) { - var coords = this._getCoords(absolute, calculate), - lines = lines || this._getImageLines(coords), - xPoints = this._findCrossPoints(point, lines); - // if xPoints is odd then point is inside the object - return (xPoints !== 0 && xPoints % 2 === 1); - }, - - /** - * Checks if object is contained within the canvas with current viewportTransform - * the check is done stopping at first point that appears on screen - * @param {Boolean} [calculate] use coordinates of current position instead of .aCoords - * @return {Boolean} true if object is fully or partially contained within canvas - */ - isOnScreen: function (calculate) { - if (!this.canvas) { - return false; - } - var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; - var points = this.getCoords(true, calculate); - // if some point is on screen, the object is on screen. - if (points.some(function (point) { - return point.x <= pointBR.x && point.x >= pointTL.x && - point.y <= pointBR.y && point.y >= pointTL.y; - })) { - return true; - } - // no points on screen, check intersection with absolute coordinates - if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { - return true; - } - return this._containsCenterOfCanvas(pointTL, pointBR, calculate); - }, - - /** - * Checks if the object contains the midpoint between canvas extremities - * Does not make sense outside the context of isOnScreen and isPartiallyOnScreen - * @private - * @param {Fabric.Point} pointTL Top Left point - * @param {Fabric.Point} pointBR Top Right point - * @param {Boolean} calculate use coordinates of current position instead of .oCoords - * @return {Boolean} true if the object contains the point - */ - _containsCenterOfCanvas: function (pointTL, pointBR, calculate) { - // worst case scenario the object is so big that contains the screen - var centerPoint = { x: (pointTL.x + pointBR.x) / 2, y: (pointTL.y + pointBR.y) / 2 }; - if (this.containsPoint(centerPoint, null, true, calculate)) { - return true; - } - return false; - }, - - /** - * Checks if object is partially contained within the canvas with current viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object is partially contained within canvas - */ - isPartiallyOnScreen: function (calculate) { - if (!this.canvas) { - return false; - } - var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; - if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { - return true; - } - var allPointsAreOutside = this.getCoords(true, calculate).every(function (point) { - return (point.x >= pointBR.x || point.x <= pointTL.x) && - (point.y >= pointBR.y || point.y <= pointTL.y); - }); - return allPointsAreOutside && this._containsCenterOfCanvas(pointTL, pointBR, calculate); - }, - - /** - * Method that returns an object with the object edges in it, given the coordinates of the corners - * @private - * @param {Object} oCoords Coordinates of the object corners - */ - _getImageLines: function (oCoords) { - - var lines = { - topline: { - o: oCoords.tl, - d: oCoords.tr - }, - rightline: { - o: oCoords.tr, - d: oCoords.br - }, - bottomline: { - o: oCoords.br, - d: oCoords.bl - }, - leftline: { - o: oCoords.bl, - d: oCoords.tl - } - }; - - // // debugging - // if (this.canvas.contextTop) { - // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - // } - - return lines; - }, - - /** - * Helper method to determine how many cross points are between the 4 object edges - * and the horizontal line determined by a point on canvas - * @private - * @param {fabric.Point} point Point to check - * @param {Object} lines Coordinates of the object being evaluated - */ - // remove yi, not used but left code here just in case. - _findCrossPoints: function (point, lines) { - var b1, b2, a1, a2, xi, // yi, - xcount = 0, - iLine; - - for (var lineKey in lines) { - iLine = lines[lineKey]; - // optimisation 1: line below point. no cross - if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { - continue; - } - // optimisation 2: line above point. no cross - if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) { - continue; - } - // optimisation 3: vertical line case - if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) { - xi = iLine.o.x; - // yi = point.y; - } - // calculate the intersection point - else { - b1 = 0; - b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); - a1 = point.y - b1 * point.x; - a2 = iLine.o.y - b2 * iLine.o.x; - - xi = -(a1 - a2) / (b1 - b2); - // yi = a1 + b1 * xi; - } - // dont count xi < point.x cases - if (xi >= point.x) { - xcount += 1; - } - // optimisation 4: specific for square images - if (xcount === 2) { - break; - } - } - return xcount; - }, - - /** - * Returns coordinates of object's bounding rectangle (left, top, width, height) - * the box is intended as aligned to axis of canvas. - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords / .aCoords - * @return {Object} Object with left, top, width, height properties - */ - getBoundingRect: function (absolute, calculate) { - var coords = this.getCoords(absolute, calculate); - return util.makeBoundingBoxFromPoints(coords); - }, - - /** - * Returns width of an object's bounding box counting transformations - * before 2.0 it was named getWidth(); - * @return {Number} width value - */ - getScaledWidth: function () { - return this._getTransformedDimensions().x; - }, - - /** - * Returns height of an object bounding box counting transformations - * before 2.0 it was named getHeight(); - * @return {Number} height value - */ - getScaledHeight: function () { - return this._getTransformedDimensions().y; - }, - - /** - * Makes sure the scale is valid and modifies it if necessary - * @private - * @param {Number} value - * @return {Number} - */ - _constrainScale: function (value) { - if (Math.abs(value) < this.minScaleLimit) { - if (value < 0) { - return -this.minScaleLimit; - } - else { - return this.minScaleLimit; - } - } - else if (value === 0) { - return 0.0001; - } - return value; - }, - - /** - * Scales an object (equally by x and y) - * @param {Number} value Scale factor - * @return {fabric.Object} thisArg - * @chainable - */ - scale: function (value) { - this._set('scaleX', value); - this._set('scaleY', value); - return this.setCoords(); - }, - - /** - * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) - * @param {Number} value New width value - * @param {Boolean} absolute ignore viewport - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToWidth: function (value, absolute) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRect(absolute).width / this.getScaledWidth(); - return this.scale(value / this.width / boundingRectFactor); - }, - - /** - * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) - * @param {Number} value New height value - * @param {Boolean} absolute ignore viewport - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToHeight: function (value, absolute) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRect(absolute).height / this.getScaledHeight(); - return this.scale(value / this.height / boundingRectFactor); - }, - - calcLineCoords: function () { - var vpt = this.getViewportTransform(), - padding = this.padding, angle = degreesToRadians(this.angle), - cos = util.cos(angle), sin = util.sin(angle), - cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, - cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); - - var lineCoords = { - tl: transformPoint(aCoords.tl, vpt), - tr: transformPoint(aCoords.tr, vpt), - bl: transformPoint(aCoords.bl, vpt), - br: transformPoint(aCoords.br, vpt), - }; - - if (padding) { - lineCoords.tl.x -= cosPMinusSinP; - lineCoords.tl.y -= cosPSinP; - lineCoords.tr.x += cosPSinP; - lineCoords.tr.y -= cosPMinusSinP; - lineCoords.bl.x -= cosPSinP; - lineCoords.bl.y += cosPMinusSinP; - lineCoords.br.x += cosPMinusSinP; - lineCoords.br.y += cosPSinP; - } - - return lineCoords; - }, - - calcOCoords: function () { - var rotateMatrix = this._calcRotateMatrix(), - translateMatrix = this._calcTranslateMatrix(), - vpt = this.getViewportTransform(), - startMatrix = multiplyMatrices(vpt, translateMatrix), - finalMatrix = multiplyMatrices(startMatrix, rotateMatrix), - finalMatrix = multiplyMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), - dim = this._calculateCurrentDimensions(), - coords = {}; - this.forEachControl(function (control, key, fabricObject) { - coords[key] = control.positionHandler(dim, finalMatrix, fabricObject); - }); - - // debug code - // var canvas = this.canvas; - // setTimeout(function() { - // canvas.contextTop.clearRect(0, 0, 700, 700); - // canvas.contextTop.fillStyle = 'green'; - // Object.keys(coords).forEach(function(key) { - // var control = coords[key]; - // canvas.contextTop.fillRect(control.x, control.y, 3, 3); - // }); - // }, 50); - return coords; - }, - - calcACoords: function () { - var rotateMatrix = this._calcRotateMatrix(), - translateMatrix = this._calcTranslateMatrix(), - finalMatrix = multiplyMatrices(translateMatrix, rotateMatrix), - dim = this._getTransformedDimensions(), - w = dim.x / 2, h = dim.y / 2; - return { - // corners - tl: transformPoint({ x: -w, y: -h }, finalMatrix), - tr: transformPoint({ x: w, y: -h }, finalMatrix), - bl: transformPoint({ x: -w, y: h }, finalMatrix), - br: transformPoint({ x: w, y: h }, finalMatrix) - }; - }, - - /** - * Sets corner and controls position coordinates based on current angle, width and height, left and top. - * oCoords are used to find the corners - * aCoords are used to quickly find an object on the canvas - * lineCoords are used to quickly find object during pointer events. - * See {@link https://github.com/fabricjs/fabric.js/wiki/When-to-call-setCoords} and {@link http://fabricjs.com/fabric-gotchas} - * - * @param {Boolean} [skipCorners] skip calculation of oCoords. - * @return {fabric.Object} thisArg - * @chainable - */ - setCoords: function (skipCorners) { - this.aCoords = this.calcACoords(); - // in case we are in a group, for how the inner group target check works, - // lineCoords are exactly aCoords. Since the vpt gets absorbed by the normalized pointer. - this.lineCoords = this.group ? this.aCoords : this.calcLineCoords(); - if (skipCorners) { - return this; - } - // set coordinates of the draggable boxes in the corners used to scale/rotate the image - this.oCoords = this.calcOCoords(); - this._setCornerCoords && this._setCornerCoords(); - return this; - }, - - /** - * calculate rotation matrix of an object - * @return {Array} rotation matrix for the object - */ - _calcRotateMatrix: function () { - return util.calcRotateMatrix(this); - }, - - /** - * calculate the translation matrix for an object transform - * @return {Array} rotation matrix for the object - */ - _calcTranslateMatrix: function () { - var center = this.getCenterPoint(); - return [1, 0, 0, 1, center.x, center.y]; - }, - - transformMatrixKey: function (skipGroup) { - var sep = '_', prefix = ''; - if (!skipGroup && this.group) { - prefix = this.group.transformMatrixKey(skipGroup) + sep; - }; - return prefix + this.top + sep + this.left + sep + this.scaleX + sep + this.scaleY + - sep + this.skewX + sep + this.skewY + sep + this.angle + sep + this.originX + sep + this.originY + - sep + this.width + sep + this.height + sep + this.strokeWidth + this.flipX + this.flipY; - }, - - /** - * calculate transform matrix that represents the current transformations from the - * object's properties. - * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations - * There are some situation in which this is useful to avoid the fake rotation. - * @return {Array} transform matrix for the object - */ - calcTransformMatrix: function (skipGroup) { - var matrix = this.calcOwnMatrix(); - if (skipGroup || !this.group) { - return matrix; - } - var key = this.transformMatrixKey(skipGroup), cache = this.matrixCache || (this.matrixCache = {}); - if (cache.key === key) { - return cache.value; - } - if (this.group) { - matrix = multiplyMatrices(this.group.calcTransformMatrix(false), matrix); - } - cache.key = key; - cache.value = matrix; - return matrix; - }, - - /** - * calculate transform matrix that represents the current transformations from the - * object's properties, this matrix does not include the group transformation - * @return {Array} transform matrix for the object - */ - calcOwnMatrix: function () { - var key = this.transformMatrixKey(true), cache = this.ownMatrixCache || (this.ownMatrixCache = {}); - if (cache.key === key) { - return cache.value; - } - var tMatrix = this._calcTranslateMatrix(), - options = { - angle: this.angle, - translateX: tMatrix[4], - translateY: tMatrix[5], - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: this.skewX, - skewY: this.skewY, - flipX: this.flipX, - flipY: this.flipY, - }; - cache.key = key; - cache.value = util.composeMatrix(options); - return cache.value; - }, - - /* - * Calculate object dimensions from its properties - * @private - * @return {Object} .x width dimension - * @return {Object} .y height dimension - */ - _getNonTransformedDimensions: function () { - var strokeWidth = this.strokeWidth, - w = this.width + strokeWidth, - h = this.height + strokeWidth; - return { x: w, y: h }; - }, - - /* - * Calculate object bounding box dimensions from its properties scale, skew. - * @param {Number} skewX, a value to override current skewX - * @param {Number} skewY, a value to override current skewY - * @private - * @return {Object} .x width dimension - * @return {Object} .y height dimension - */ - _getTransformedDimensions: function (skewX, skewY) { - if (typeof skewX === 'undefined') { - skewX = this.skewX; - } - if (typeof skewY === 'undefined') { - skewY = this.skewY; - } - var dimensions, dimX, dimY, - noSkew = skewX === 0 && skewY === 0; - - if (this.strokeUniform) { - dimX = this.width; - dimY = this.height; - } - else { - dimensions = this._getNonTransformedDimensions(); - dimX = dimensions.x; - dimY = dimensions.y; - } - if (noSkew) { - return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); - } - var bbox = util.sizeAfterTransform(dimX, dimY, { - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: skewX, - skewY: skewY, - }); - return this._finalizeDimensions(bbox.x, bbox.y); - }, - - /* - * Calculate object bounding box dimensions from its properties scale, skew. - * @param Number width width of the bbox - * @param Number height height of the bbox - * @private - * @return {Object} .x finalized width dimension - * @return {Object} .y finalized height dimension - */ - _finalizeDimensions: function (width, height) { - return this.strokeUniform ? - { x: width + this.strokeWidth, y: height + this.strokeWidth } - : - { x: width, y: height }; - }, - - /* - * Calculate object dimensions for controls box, including padding and canvas zoom. - * and active selection - * private - */ - _calculateCurrentDimensions: function () { - var vpt = this.getViewportTransform(), - dim = this._getTransformedDimensions(), - p = transformPoint(dim, vpt, true); - return p.scalarAdd(2 * this.padding); - }, - }); -})(); - - -fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - - /** - * Moves an object to the bottom of the stack of drawn objects - * @return {fabric.Object} thisArg - * @chainable - */ - sendToBack: function () { - if (this.group) { - fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); - } - else if (this.canvas) { - this.canvas.sendToBack(this); - } - return this; - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @return {fabric.Object} thisArg - * @chainable - */ - bringToFront: function () { - if (this.group) { - fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); - } - else if (this.canvas) { - this.canvas.bringToFront(this); - } - return this; - }, - - /** - * Moves an object down in stack of drawn objects - * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object - * @return {fabric.Object} thisArg - * @chainable - */ - sendBackwards: function (intersecting) { - if (this.group) { - fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); - } - else if (this.canvas) { - this.canvas.sendBackwards(this, intersecting); - } - return this; - }, - - /** - * Moves an object up in stack of drawn objects - * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object - * @return {fabric.Object} thisArg - * @chainable - */ - bringForward: function (intersecting) { - if (this.group) { - fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); - } - else if (this.canvas) { - this.canvas.bringForward(this, intersecting); - } - return this; - }, - - /** - * Moves an object to specified level in stack of drawn objects - * @param {Number} index New position of object - * @return {fabric.Object} thisArg - * @chainable - */ - moveTo: function (index) { - if (this.group && this.group.type !== 'activeSelection') { - fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); - } - else if (this.canvas) { - this.canvas.moveTo(this, index); - } - return this; - } -}); - - -/* _TO_SVG_START_ */ -(function () { - function getSvgColorString(prop, value) { - if (!value) { - return prop + ': none; '; - } - else if (value.toLive) { - return prop + ': url(#SVGID_' + value.id + '); '; - } - else { - var color = new fabric.Color(value), - str = prop + ': ' + color.toRgb() + '; ', - opacity = color.getAlpha(); - if (opacity !== 1) { - //change the color in rgb + opacity - str += prop + '-opacity: ' + opacity.toString() + '; '; - } - return str; - } - } - - var toFixed = fabric.util.toFixed; - - fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - /** - * Returns styles-string for svg-export - * @param {Boolean} skipShadow a boolean to skip shadow filter output - * @return {String} - */ - getSvgStyles: function (skipShadow) { - - var fillRule = this.fillRule ? this.fillRule : 'nonzero', - strokeWidth = this.strokeWidth ? this.strokeWidth : '0', - strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none', - strokeDashOffset = this.strokeDashOffset ? this.strokeDashOffset : '0', - strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt', - strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter', - strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4', - opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1', - visibility = this.visible ? '' : ' visibility: hidden;', - filter = skipShadow ? '' : this.getSvgFilter(), - fill = getSvgColorString('fill', this.fill), - stroke = getSvgColorString('stroke', this.stroke); - - return [ - stroke, - 'stroke-width: ', strokeWidth, '; ', - 'stroke-dasharray: ', strokeDashArray, '; ', - 'stroke-linecap: ', strokeLineCap, '; ', - 'stroke-dashoffset: ', strokeDashOffset, '; ', - 'stroke-linejoin: ', strokeLineJoin, '; ', - 'stroke-miterlimit: ', strokeMiterLimit, '; ', - fill, - 'fill-rule: ', fillRule, '; ', - 'opacity: ', opacity, ';', - filter, - visibility - ].join(''); - }, - - /** - * Returns styles-string for svg-export - * @param {Object} style the object from which to retrieve style properties - * @param {Boolean} useWhiteSpace a boolean to include an additional attribute in the style. - * @return {String} - */ - getSvgSpanStyles: function (style, useWhiteSpace) { - var term = '; '; - var fontFamily = style.fontFamily ? - 'font-family: ' + (((style.fontFamily.indexOf('\'') === -1 && style.fontFamily.indexOf('"') === -1) ? - '\'' + style.fontFamily + '\'' : style.fontFamily)) + term : ''; - var strokeWidth = style.strokeWidth ? 'stroke-width: ' + style.strokeWidth + term : '', - fontFamily = fontFamily, - fontSize = style.fontSize ? 'font-size: ' + style.fontSize + 'px' + term : '', - fontStyle = style.fontStyle ? 'font-style: ' + style.fontStyle + term : '', - fontWeight = style.fontWeight ? 'font-weight: ' + style.fontWeight + term : '', - fill = style.fill ? getSvgColorString('fill', style.fill) : '', - stroke = style.stroke ? getSvgColorString('stroke', style.stroke) : '', - textDecoration = this.getSvgTextDecoration(style), - deltaY = style.deltaY ? 'baseline-shift: ' + (-style.deltaY) + '; ' : ''; - if (textDecoration) { - textDecoration = 'text-decoration: ' + textDecoration + term; - } - - return [ - stroke, - strokeWidth, - fontFamily, - fontSize, - fontStyle, - fontWeight, - textDecoration, - fill, - deltaY, - useWhiteSpace ? 'white-space: pre; ' : '' - ].join(''); - }, - - /** - * Returns text-decoration property for svg-export - * @param {Object} style the object from which to retrieve style properties - * @return {String} - */ - getSvgTextDecoration: function (style) { - return ['overline', 'underline', 'line-through'].filter(function (decoration) { - return style[decoration.replace('-', '')]; - }).join(' '); - }, - - /** - * Returns filter for svg shadow - * @return {String} - */ - getSvgFilter: function () { - return this.shadow ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; - }, - - /** - * Returns id attribute for svg output - * @return {String} - */ - getSvgCommons: function () { - return [ - this.id ? 'id="' + this.id + '" ' : '', - this.clipPath ? 'clip-path="url(#' + this.clipPath.clipPathId + ')" ' : '', - ].join(''); - }, - - /** - * Returns transform-string for svg-export - * @param {Boolean} use the full transform or the single object one. - * @return {String} - */ - getSvgTransform: function (full, additionalTransform) { - var transform = full ? this.calcTransformMatrix() : this.calcOwnMatrix(), - svgTransform = 'transform="' + fabric.util.matrixToSVG(transform); - return svgTransform + - (additionalTransform || '') + '" '; - }, - - _setSVGBg: function (textBgRects) { - if (this.backgroundColor) { - var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - textBgRects.push( - '\t\t\n'); - } - }, - - /** - * Returns svg representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - toSVG: function (reviver) { - return this._createBaseSVGMarkup(this._toSVG(reviver), { reviver: reviver }); - }, - - /** - * Returns svg clipPath representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - toClipPathSVG: function (reviver) { - return '\t' + this._createBaseClipPathSVGMarkup(this._toSVG(reviver), { reviver: reviver }); - }, - - /** - * @private - */ - _createBaseClipPathSVGMarkup: function (objectMarkup, options) { - options = options || {}; - var reviver = options.reviver, - additionalTransform = options.additionalTransform || '', - commonPieces = [ - this.getSvgTransform(true, additionalTransform), - this.getSvgCommons(), - ].join(''), - // insert commons in the markup, style and svgCommons - index = objectMarkup.indexOf('COMMON_PARTS'); - objectMarkup[index] = commonPieces; - return reviver ? reviver(objectMarkup.join('')) : objectMarkup.join(''); - }, - - /** - * @private - */ - _createBaseSVGMarkup: function (objectMarkup, options) { - options = options || {}; - var noStyle = options.noStyle, - reviver = options.reviver, - styleInfo = noStyle ? '' : 'style="' + this.getSvgStyles() + '" ', - shadowInfo = options.withShadow ? 'style="' + this.getSvgFilter() + '" ' : '', - clipPath = this.clipPath, - vectorEffect = this.strokeUniform ? 'vector-effect="non-scaling-stroke" ' : '', - absoluteClipPath = clipPath && clipPath.absolutePositioned, - stroke = this.stroke, fill = this.fill, shadow = this.shadow, - commonPieces, markup = [], clipPathMarkup, - // insert commons in the markup, style and svgCommons - index = objectMarkup.indexOf('COMMON_PARTS'), - additionalTransform = options.additionalTransform; - if (clipPath) { - clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; - clipPathMarkup = '\n' + - clipPath.toClipPathSVG(reviver) + - '\n'; - } - if (absoluteClipPath) { - markup.push( - '\n' - ); - } - markup.push( - '\n' - ); - commonPieces = [ - styleInfo, - vectorEffect, - noStyle ? '' : this.addPaintOrder(), ' ', - additionalTransform ? 'transform="' + additionalTransform + '" ' : '', - ].join(''); - objectMarkup[index] = commonPieces; - if (fill && fill.toLive) { - markup.push(fill.toSVG(this)); - } - if (stroke && stroke.toLive) { - markup.push(stroke.toSVG(this)); - } - if (shadow) { - markup.push(shadow.toSVG(this)); - } - if (clipPath) { - markup.push(clipPathMarkup); - } - markup.push(objectMarkup.join('')); - markup.push('\n'); - absoluteClipPath && markup.push('\n'); - return reviver ? reviver(markup.join('')) : markup.join(''); - }, - - addPaintOrder: function () { - return this.paintFirst !== 'fill' ? ' paint-order="' + this.paintFirst + '" ' : ''; - } - }); -})(); -/* _TO_SVG_END_ */ - - -(function () { - - var extend = fabric.util.object.extend, - originalSet = 'stateProperties'; - - /* - Depends on `stateProperties` - */ - function saveProps(origin, destination, props) { - var tmpObj = {}, deep = true; - props.forEach(function (prop) { - tmpObj[prop] = origin[prop]; - }); - - extend(origin[destination], tmpObj, deep); - } - - function _isEqual(origValue, currentValue, firstPass) { - if (origValue === currentValue) { - // if the objects are identical, return - return true; - } - else if (Array.isArray(origValue)) { - if (!Array.isArray(currentValue) || origValue.length !== currentValue.length) { - return false; - } - for (var i = 0, len = origValue.length; i < len; i++) { - if (!_isEqual(origValue[i], currentValue[i])) { - return false; - } - } - return true; - } - else if (origValue && typeof origValue === 'object') { - var keys = Object.keys(origValue), key; - if (!currentValue || - typeof currentValue !== 'object' || - (!firstPass && keys.length !== Object.keys(currentValue).length) - ) { - return false; - } - for (var i = 0, len = keys.length; i < len; i++) { - key = keys[i]; - // since clipPath is in the statefull cache list and the clipPath objects - // would be iterated as an object, this would lead to possible infinite recursion - // we do not want to compare those. - if (key === 'canvas' || key === 'group') { - continue; - } - if (!_isEqual(origValue[key], currentValue[key])) { - return false; - } - } - return true; - } - } - - - fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - - /** - * Returns true if object state (one of its state properties) was changed - * @param {String} [propertySet] optional name for the set of property we want to save - * @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called - */ - hasStateChanged: function (propertySet) { - propertySet = propertySet || originalSet; - var dashedPropertySet = '_' + propertySet; - if (Object.keys(this[dashedPropertySet]).length < this[propertySet].length) { - return true; - } - return !_isEqual(this[dashedPropertySet], this, true); - }, - - /** - * Saves state of an object - * @param {Object} [options] Object with additional `stateProperties` array to include when saving state - * @return {fabric.Object} thisArg - */ - saveState: function (options) { - var propertySet = options && options.propertySet || originalSet, - destination = '_' + propertySet; - if (!this[destination]) { - return this.setupState(options); - } - saveProps(this, destination, this[propertySet]); - if (options && options.stateProperties) { - saveProps(this, destination, options.stateProperties); - } - return this; - }, - - /** - * Setups state of an object - * @param {Object} [options] Object with additional `stateProperties` array to include when saving state - * @return {fabric.Object} thisArg - */ - setupState: function (options) { - options = options || {}; - var propertySet = options.propertySet || originalSet; - options.propertySet = propertySet; - this['_' + propertySet] = {}; - this.saveState(options); - return this; - } - }); -})(); - - -(function () { - - var degreesToRadians = fabric.util.degreesToRadians; - - fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - /** - * Determines which corner has been clicked - * @private - * @param {Object} pointer The pointer indicating the mouse position - * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found - */ - _findTargetCorner: function (pointer, forTouch) { - // objects in group, anykind, are not self modificable, - // must not return an hovered corner. - if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { - return false; - } - - var ex = pointer.x, - ey = pointer.y, - xPoints, - lines, keys = Object.keys(this.oCoords), - j = keys.length - 1, i; - this.__corner = 0; - - // cycle in reverse order so we pick first the one on top - for (; j >= 0; j--) { - i = keys[j]; - if (!this.isControlVisible(i)) { - continue; - } - - lines = this._getImageLines(forTouch ? this.oCoords[i].touchCorner : this.oCoords[i].corner); - // // debugging - // - // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - - xPoints = this._findCrossPoints({ x: ex, y: ey }, lines); - if (xPoints !== 0 && xPoints % 2 === 1) { - this.__corner = i; - return i; - } - } - return false; - }, - - /** - * Calls a function for each control. The function gets called, - * with the control, the object that is calling the iterator and the control's key - * @param {Function} fn function to iterate over the controls over - */ - forEachControl: function (fn) { - for (var i in this.controls) { - fn(this.controls[i], i, this); - }; - }, - - /** - * Sets the coordinates of the draggable boxes in the corners of - * the image used to scale/rotate it. - * note: if we would switch to ROUND corner area, all of this would disappear. - * everything would resolve to a single point and a pythagorean theorem for the distance - * @private - */ - _setCornerCoords: function () { - var coords = this.oCoords; - - for (var control in coords) { - var controlObject = this.controls[control]; - coords[control].corner = controlObject.calcCornerCoords( - this.angle, this.cornerSize, coords[control].x, coords[control].y, false); - coords[control].touchCorner = controlObject.calcCornerCoords( - this.angle, this.touchCornerSize, coords[control].x, coords[control].y, true); - } - }, - - /** - * Draws a colored layer behind the object, inside its selection borders. - * Requires public options: padding, selectionBackgroundColor - * this function is called when the context is transformed - * has checks to be skipped when the object is on a staticCanvas - * @param {CanvasRenderingContext2D} ctx Context to draw on - * @return {fabric.Object} thisArg - * @chainable - */ - drawSelectionBackground: function (ctx) { - if (!this.selectionBackgroundColor || - (this.canvas && !this.canvas.interactive) || - (this.canvas && this.canvas._activeObject !== this) - ) { - return this; - } - ctx.save(); - var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), - vpt = this.canvas.viewportTransform; - ctx.translate(center.x, center.y); - ctx.scale(1 / vpt[0], 1 / vpt[3]); - ctx.rotate(degreesToRadians(this.angle)); - ctx.fillStyle = this.selectionBackgroundColor; - ctx.fillRect(-wh.x / 2, -wh.y / 2, wh.x, wh.y); - ctx.restore(); - return this; - }, - - /** - * Draws borders of an object's bounding box. - * Requires public properties: width, height - * Requires public options: padding, borderColor - * @param {CanvasRenderingContext2D} ctx Context to draw on - * @param {Object} styleOverride object to override the object style - * @return {fabric.Object} thisArg - * @chainable - */ - drawBorders: function (ctx, styleOverride) { - styleOverride = styleOverride || {}; - var wh = this._calculateCurrentDimensions(), - strokeWidth = this.borderScaleFactor, - width = wh.x + strokeWidth, - height = wh.y + strokeWidth, - hasControls = typeof styleOverride.hasControls !== 'undefined' ? - styleOverride.hasControls : this.hasControls, - shouldStroke = false; - - ctx.save(); - ctx.strokeStyle = styleOverride.borderColor || this.borderColor; - this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); - - ctx.strokeRect( - -width / 2, - -height / 2, - width, - height - ); - - if (hasControls) { - ctx.beginPath(); - this.forEachControl(function (control, key, fabricObject) { - // in this moment, the ctx is centered on the object. - // width and height of the above function are the size of the bbox. - if (control.withConnection && control.getVisibility(fabricObject, key)) { - // reset movement for each control - shouldStroke = true; - ctx.moveTo(control.x * width, control.y * height); - ctx.lineTo( - control.x * width + control.offsetX, - control.y * height + control.offsetY - ); - } - }); - if (shouldStroke) { - ctx.stroke(); - } - } - ctx.restore(); - return this; - }, - - /** - * Draws borders of an object's bounding box when it is inside a group. - * Requires public properties: width, height - * Requires public options: padding, borderColor - * @param {CanvasRenderingContext2D} ctx Context to draw on - * @param {object} options object representing current object parameters - * @param {Object} styleOverride object to override the object style - * @return {fabric.Object} thisArg - * @chainable - */ - drawBordersInGroup: function (ctx, options, styleOverride) { - styleOverride = styleOverride || {}; - var bbox = fabric.util.sizeAfterTransform(this.width, this.height, options), - strokeWidth = this.strokeWidth, - strokeUniform = this.strokeUniform, - borderScaleFactor = this.borderScaleFactor, - width = - bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, - height = - bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; - ctx.save(); - this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); - ctx.strokeStyle = styleOverride.borderColor || this.borderColor; - ctx.strokeRect( - -width / 2, - -height / 2, - width, - height - ); - - ctx.restore(); - return this; - }, - - /** - * Draws corners of an object's bounding box. - * Requires public properties: width, height - * Requires public options: cornerSize, padding - * @param {CanvasRenderingContext2D} ctx Context to draw on - * @param {Object} styleOverride object to override the object style - * @return {fabric.Object} thisArg - * @chainable - */ - drawControls: function (ctx, styleOverride) { - styleOverride = styleOverride || {}; - ctx.save(); - var retinaScaling = this.canvas.getRetinaScaling(), matrix, p; - ctx.setTransform(retinaScaling, 0, 0, retinaScaling, 0, 0); - ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; - if (!this.transparentCorners) { - ctx.strokeStyle = styleOverride.cornerStrokeColor || this.cornerStrokeColor; - } - this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray); - this.setCoords(); - if (this.group) { - // fabricJS does not really support drawing controls inside groups, - // this piece of code here helps having at least the control in places. - // If an application needs to show some objects as selected because of some UI state - // can still call Object._renderControls() on any object they desire, independently of groups. - // using no padding, circular controls and hiding the rotating cursor is higly suggested, - matrix = this.group.calcTransformMatrix(); - } - this.forEachControl(function (control, key, fabricObject) { - p = fabricObject.oCoords[key]; - if (control.getVisibility(fabricObject, key)) { - if (matrix) { - p = fabric.util.transformPoint(p, matrix); - } - control.render(ctx, p.x, p.y, styleOverride, fabricObject); - } - }); - ctx.restore(); - - return this; - }, - - /** - * Returns true if the specified control is visible, false otherwise. - * @param {String} controlKey The key of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. - * @returns {Boolean} true if the specified control is visible, false otherwise - */ - isControlVisible: function (controlKey) { - return this.controls[controlKey] && this.controls[controlKey].getVisibility(this, controlKey); - }, - - /** - * Sets the visibility of the specified control. - * @param {String} controlKey The key of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. - * @param {Boolean} visible true to set the specified control visible, false otherwise - * @return {fabric.Object} thisArg - * @chainable - */ - setControlVisible: function (controlKey, visible) { - if (!this._controlsVisibility) { - this._controlsVisibility = {}; - } - this._controlsVisibility[controlKey] = visible; - return this; - }, - - /** - * Sets the visibility state of object controls. - * @param {Object} [options] Options object - * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it - * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it - * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it - * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it - * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it - * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it - * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it - * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it - * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it - * @return {fabric.Object} thisArg - * @chainable - */ - setControlsVisibility: function (options) { - options || (options = {}); - - for (var p in options) { - this.setControlVisible(p, options[p]); - } - return this; - }, - - - /** - * This callback function is called every time _discardActiveObject or _setActiveObject - * try to to deselect this object. If the function returns true, the process is cancelled - * @param {Object} [options] options sent from the upper functions - * @param {Event} [options.e] event if the process is generated by an event - */ - onDeselect: function () { - // implemented by sub-classes, as needed. - }, - - - /** - * This callback function is called every time _discardActiveObject or _setActiveObject - * try to to select this object. If the function returns true, the process is cancelled - * @param {Object} [options] options sent from the upper functions - * @param {Event} [options.e] event if the process is generated by an event - */ - onSelect: function () { - // implemented by sub-classes, as needed. - } - }); -})(); - - -fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { - - /** - * Animation duration (in ms) for fx* methods - * @type Number - * @default - */ - FX_DURATION: 500, - - /** - * Centers object horizontally with animation. - * @param {fabric.Object} object Object to center - * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties - * @param {Function} [callbacks.onComplete] Invoked on completion - * @param {Function} [callbacks.onChange] Invoked on every step of animation - * @return {fabric.AnimationContext} context - */ - fxCenterObjectH: function (object, callbacks) { - callbacks = callbacks || {}; - - var empty = function () { }, - onComplete = callbacks.onComplete || empty, - onChange = callbacks.onChange || empty, - _this = this; - - return fabric.util.animate({ - target: this, - startValue: object.left, - endValue: this.getCenterPoint().x, - duration: this.FX_DURATION, - onChange: function (value) { - object.set('left', value); - _this.requestRenderAll(); - onChange(); - }, - onComplete: function () { - object.setCoords(); - onComplete(); - } - }); - }, - - /** - * Centers object vertically with animation. - * @param {fabric.Object} object Object to center - * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties - * @param {Function} [callbacks.onComplete] Invoked on completion - * @param {Function} [callbacks.onChange] Invoked on every step of animation - * @return {fabric.AnimationContext} context - */ - fxCenterObjectV: function (object, callbacks) { - callbacks = callbacks || {}; - - var empty = function () { }, - onComplete = callbacks.onComplete || empty, - onChange = callbacks.onChange || empty, - _this = this; - - return fabric.util.animate({ - target: this, - startValue: object.top, - endValue: this.getCenterPoint().y, - duration: this.FX_DURATION, - onChange: function (value) { - object.set('top', value); - _this.requestRenderAll(); - onChange(); - }, - onComplete: function () { - object.setCoords(); - onComplete(); - } - }); - }, - - /** - * Same as `fabric.Canvas#remove` but animated - * @param {fabric.Object} object Object to remove - * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties - * @param {Function} [callbacks.onComplete] Invoked on completion - * @param {Function} [callbacks.onChange] Invoked on every step of animation - * @return {fabric.AnimationContext} context - */ - fxRemove: function (object, callbacks) { - callbacks = callbacks || {}; - - var empty = function () { }, - onComplete = callbacks.onComplete || empty, - onChange = callbacks.onChange || empty, - _this = this; - - return fabric.util.animate({ - target: this, - startValue: object.opacity, - endValue: 0, - duration: this.FX_DURATION, - onChange: function (value) { - object.set('opacity', value); - _this.requestRenderAll(); - onChange(); - }, - onComplete: function () { - _this.remove(object); - onComplete(); - } - }); - } -}); - -fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - /** - * Animates object's properties - * @param {String|Object} property Property to animate (if string) or properties to animate (if object) - * @param {Number|Object} value Value to animate property to (if string was given first) or options object - * @return {fabric.Object} thisArg - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#animation} - * @return {fabric.AnimationContext | fabric.AnimationContext[]} animation context (or an array if passed multiple properties) - * - * As object — multiple properties - * - * object.animate({ left: ..., top: ... }); - * object.animate({ left: ..., top: ... }, { duration: ... }); - * - * As string — one property - * - * object.animate('left', ...); - * object.animate('left', { duration: ... }); - * - */ - animate: function () { - if (arguments[0] && typeof arguments[0] === 'object') { - var propsToAnimate = [], prop, skipCallbacks, out = []; - for (prop in arguments[0]) { - propsToAnimate.push(prop); - } - for (var i = 0, len = propsToAnimate.length; i < len; i++) { - prop = propsToAnimate[i]; - skipCallbacks = i !== len - 1; - out.push(this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks)); - } - return out; - } - else { - return this._animate.apply(this, arguments); - } - }, - - /** - * @private - * @param {String} property Property to animate - * @param {String} to Value to animate to - * @param {Object} [options] Options object - * @param {Boolean} [skipCallbacks] When true, callbacks like onchange and oncomplete are not invoked - */ - _animate: function (property, to, options, skipCallbacks) { - var _this = this, propPair; - - to = to.toString(); - - if (!options) { - options = {}; - } - else { - options = fabric.util.object.clone(options); - } - - if (~property.indexOf('.')) { - propPair = property.split('.'); - } - - var propIsColor = - _this.colorProperties.indexOf(property) > -1 || - (propPair && _this.colorProperties.indexOf(propPair[1]) > -1); - - var currentValue = propPair - ? this.get(propPair[0])[propPair[1]] - : this.get(property); - - if (!('from' in options)) { - options.from = currentValue; - } - - if (!propIsColor) { - if (~to.indexOf('=')) { - to = currentValue + parseFloat(to.replace('=', '')); - } - else { - to = parseFloat(to); - } - } - - var _options = { - target: this, - startValue: options.from, - endValue: to, - byValue: options.by, - easing: options.easing, - duration: options.duration, - abort: options.abort && function (value, valueProgress, timeProgress) { - return options.abort.call(_this, value, valueProgress, timeProgress); - }, - onChange: function (value, valueProgress, timeProgress) { - if (propPair) { - _this[propPair[0]][propPair[1]] = value; - } - else { - _this.set(property, value); - } - if (skipCallbacks) { - return; - } - options.onChange && options.onChange(value, valueProgress, timeProgress); - }, - onComplete: function (value, valueProgress, timeProgress) { - if (skipCallbacks) { - return; - } - - _this.setCoords(); - options.onComplete && options.onComplete(value, valueProgress, timeProgress); - } - }; - - if (propIsColor) { - return fabric.util.animateColor(_options.startValue, _options.endValue, _options.duration, _options); - } - else { - return fabric.util.animate(_options); - } - } -}); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - extend = fabric.util.object.extend, - clone = fabric.util.object.clone, - coordProps = { x1: 1, x2: 1, y1: 1, y2: 1 }; - - if (fabric.Line) { - fabric.warn('fabric.Line is already defined'); - return; - } - - /** - * Line class - * @class fabric.Line - * @extends fabric.Object - * @see {@link fabric.Line#initialize} for constructor definition - */ - fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'line', - - /** - * x value or first line edge - * @type Number - * @default - */ - x1: 0, - - /** - * y value or first line edge - * @type Number - * @default - */ - y1: 0, - - /** - * x value or second line edge - * @type Number - * @default - */ - x2: 0, - - /** - * y value or second line edge - * @type Number - * @default - */ - y2: 0, - - cacheProperties: fabric.Object.prototype.cacheProperties.concat('x1', 'x2', 'y1', 'y2'), - - /** - * Constructor - * @param {Array} [points] Array of points - * @param {Object} [options] Options object - * @return {fabric.Line} thisArg - */ - initialize: function (points, options) { - if (!points) { - points = [0, 0, 0, 0]; - } - - this.callSuper('initialize', options); - - this.set('x1', points[0]); - this.set('y1', points[1]); - this.set('x2', points[2]); - this.set('y2', points[3]); - - this._setWidthHeight(options); - }, - - /** - * @private - * @param {Object} [options] Options - */ - _setWidthHeight: function (options) { - options || (options = {}); - - this.width = Math.abs(this.x2 - this.x1); - this.height = Math.abs(this.y2 - this.y1); - - this.left = 'left' in options - ? options.left - : this._getLeftToOriginX(); - - this.top = 'top' in options - ? options.top - : this._getTopToOriginY(); - }, - - /** - * @private - * @param {String} key - * @param {*} value - */ - _set: function (key, value) { - this.callSuper('_set', key, value); - if (typeof coordProps[key] !== 'undefined') { - this._setWidthHeight(); - } - return this; - }, - - /** - * @private - * @return {Number} leftToOriginX Distance from left edge of canvas to originX of Line. - */ - _getLeftToOriginX: makeEdgeToOriginGetter( - { // property names - origin: 'originX', - axis1: 'x1', - axis2: 'x2', - dimension: 'width' - }, - { // possible values of origin - nearest: 'left', - center: 'center', - farthest: 'right' - } - ), - - /** - * @private - * @return {Number} topToOriginY Distance from top edge of canvas to originY of Line. - */ - _getTopToOriginY: makeEdgeToOriginGetter( - { // property names - origin: 'originY', - axis1: 'y1', - axis2: 'y2', - dimension: 'height' - }, - { // possible values of origin - nearest: 'top', - center: 'center', - farthest: 'bottom' - } - ), - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - ctx.beginPath(); - - - var p = this.calcLinePoints(); - ctx.moveTo(p.x1, p.y1); - ctx.lineTo(p.x2, p.y2); - - ctx.lineWidth = this.strokeWidth; - - // TODO: test this - // make sure setting "fill" changes color of a line - // (by copying fillStyle to strokeStyle, since line is stroked, not filled) - var origStrokeStyle = ctx.strokeStyle; - ctx.strokeStyle = this.stroke || ctx.fillStyle; - this.stroke && this._renderStroke(ctx); - ctx.strokeStyle = origStrokeStyle; - }, - - /** - * This function is an helper for svg import. it returns the center of the object in the svg - * untransformed coordinates - * @private - * @return {Object} center point from element coordinates - */ - _findCenterFromElement: function () { - return { - x: (this.x1 + this.x2) / 2, - y: (this.y1 + this.y2) / 2, - }; - }, - - /** - * Returns object representation of an instance - * @method toObject - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - return extend(this.callSuper('toObject', propertiesToInclude), this.calcLinePoints()); - }, - - /* - * Calculate object dimensions from its properties - * @private - */ - _getNonTransformedDimensions: function () { - var dim = this.callSuper('_getNonTransformedDimensions'); - if (this.strokeLineCap === 'butt') { - if (this.width === 0) { - dim.y -= this.strokeWidth; - } - if (this.height === 0) { - dim.x -= this.strokeWidth; - } - } - return dim; - }, - - /** - * Recalculates line points given width and height - * @private - */ - calcLinePoints: function () { - var xMult = this.x1 <= this.x2 ? -1 : 1, - yMult = this.y1 <= this.y2 ? -1 : 1, - x1 = (xMult * this.width * 0.5), - y1 = (yMult * this.height * 0.5), - x2 = (xMult * this.width * -0.5), - y2 = (yMult * this.height * -0.5); - - return { - x1: x1, - x2: x2, - y1: y1, - y2: y2 - }; - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - var p = this.calcLinePoints(); - return [ - '\n' - ]; - }, - /* _TO_SVG_END_ */ - }); - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Line.fromElement}) - * @static - * @memberOf fabric.Line - * @see http://www.w3.org/TR/SVG/shapes.html#LineElement - */ - fabric.Line.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x1 y1 x2 y2'.split(' ')); - - /** - * Returns fabric.Line instance from an SVG element - * @static - * @memberOf fabric.Line - * @param {SVGElement} element Element to parse - * @param {Object} [options] Options object - * @param {Function} [callback] callback function invoked after parsing - */ - fabric.Line.fromElement = function (element, callback, options) { - options = options || {}; - var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES), - points = [ - parsedAttributes.x1 || 0, - parsedAttributes.y1 || 0, - parsedAttributes.x2 || 0, - parsedAttributes.y2 || 0 - ]; - callback(new fabric.Line(points, extend(parsedAttributes, options))); - }; - /* _FROM_SVG_END_ */ - - /** - * Returns fabric.Line instance from an object representation - * @static - * @memberOf fabric.Line - * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument - */ - fabric.Line.fromObject = function (object, callback) { - function _callback(instance) { - delete instance.points; - callback && callback(instance); - }; - var options = clone(object, true); - options.points = [object.x1, object.y1, object.x2, object.y2]; - fabric.Object._fromObject('Line', options, _callback, 'points'); - }; - - /** - * Produces a function that calculates distance from canvas edge to Line origin. - */ - function makeEdgeToOriginGetter(propertyNames, originValues) { - var origin = propertyNames.origin, - axis1 = propertyNames.axis1, - axis2 = propertyNames.axis2, - dimension = propertyNames.dimension, - nearest = originValues.nearest, - center = originValues.center, - farthest = originValues.farthest; - - return function () { - switch (this.get(origin)) { - case nearest: - return Math.min(this.get(axis1), this.get(axis2)); - case center: - return Math.min(this.get(axis1), this.get(axis2)) + (0.5 * this.get(dimension)); - case farthest: - return Math.max(this.get(axis1), this.get(axis2)); - } - }; - - } - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - degreesToRadians = fabric.util.degreesToRadians; - - if (fabric.Circle) { - fabric.warn('fabric.Circle is already defined.'); - return; - } - - /** - * Circle class - * @class fabric.Circle - * @extends fabric.Object - * @see {@link fabric.Circle#initialize} for constructor definition - */ - fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'circle', - - /** - * Radius of this circle - * @type Number - * @default - */ - radius: 0, - - /** - * degrees of start of the circle. - * probably will change to degrees in next major version - * @type Number 0 - 359 - * @default 0 - */ - startAngle: 0, - - /** - * End angle of the circle - * probably will change to degrees in next major version - * @type Number 1 - 360 - * @default 360 - */ - endAngle: 360, - - cacheProperties: fabric.Object.prototype.cacheProperties.concat('radius', 'startAngle', 'endAngle'), - - /** - * @private - * @param {String} key - * @param {*} value - * @return {fabric.Circle} thisArg - */ - _set: function (key, value) { - this.callSuper('_set', key, value); - - if (key === 'radius') { - this.setRadius(value); - } - - return this; - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - return this.callSuper('toObject', ['radius', 'startAngle', 'endAngle'].concat(propertiesToInclude)); - }, - - /* _TO_SVG_START_ */ - - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - var svgString, x = 0, y = 0, - angle = (this.endAngle - this.startAngle) % 360; - - if (angle === 0) { - svgString = [ - '\n' - ]; - } - else { - var start = degreesToRadians(this.startAngle), - end = degreesToRadians(this.endAngle), - radius = this.radius, - startX = fabric.util.cos(start) * radius, - startY = fabric.util.sin(start) * radius, - endX = fabric.util.cos(end) * radius, - endY = fabric.util.sin(end) * radius, - largeFlag = angle > 180 ? '1' : '0'; - svgString = [ - '\n' - ]; - } - return svgString; - }, - /* _TO_SVG_END_ */ - - /** - * @private - * @param {CanvasRenderingContext2D} ctx context to render on - */ - _render: function (ctx) { - ctx.beginPath(); - ctx.arc( - 0, - 0, - this.radius, - degreesToRadians(this.startAngle), - degreesToRadians(this.endAngle), - false - ); - this._renderPaintInOrder(ctx); - }, - - /** - * Returns horizontal radius of an object (according to how an object is scaled) - * @return {Number} - */ - getRadiusX: function () { - return this.get('radius') * this.get('scaleX'); - }, - - /** - * Returns vertical radius of an object (according to how an object is scaled) - * @return {Number} - */ - getRadiusY: function () { - return this.get('radius') * this.get('scaleY'); - }, - - /** - * Sets radius of an object (and updates width accordingly) - * @return {fabric.Circle} thisArg - */ - setRadius: function (value) { - this.radius = value; - return this.set('width', value * 2).set('height', value * 2); - }, - }); - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) - * @static - * @memberOf fabric.Circle - * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement - */ - fabric.Circle.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy r'.split(' ')); - - /** - * Returns {@link fabric.Circle} instance from an SVG element - * @static - * @memberOf fabric.Circle - * @param {SVGElement} element Element to parse - * @param {Function} [callback] Options callback invoked after parsing is finished - * @param {Object} [options] Options object - * @throws {Error} If value of `r` attribute is missing or invalid - */ - fabric.Circle.fromElement = function (element, callback) { - var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES); - - if (!isValidRadius(parsedAttributes)) { - throw new Error('value of `r` attribute is required and can not be negative'); - } - - parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.radius; - parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.radius; - callback(new fabric.Circle(parsedAttributes)); - }; - - /** - * @private - */ - function isValidRadius(attributes) { - return (('radius' in attributes) && (attributes.radius >= 0)); - } - /* _FROM_SVG_END_ */ - - /** - * Returns {@link fabric.Circle} instance from an object representation - * @static - * @memberOf fabric.Circle - * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument - * @return {void} - */ - fabric.Circle.fromObject = function (object, callback) { - fabric.Object._fromObject('Circle', object, callback); - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}); - - if (fabric.Triangle) { - fabric.warn('fabric.Triangle is already defined'); - return; - } - - /** - * Triangle class - * @class fabric.Triangle - * @extends fabric.Object - * @return {fabric.Triangle} thisArg - * @see {@link fabric.Triangle#initialize} for constructor definition - */ - fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'triangle', - - /** - * Width is set to 100 to compensate the old initialize code that was setting it to 100 - * @type Number - * @default - */ - width: 100, - - /** - * Height is set to 100 to compensate the old initialize code that was setting it to 100 - * @type Number - * @default - */ - height: 100, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - var widthBy2 = this.width / 2, - heightBy2 = this.height / 2; - - ctx.beginPath(); - ctx.moveTo(-widthBy2, heightBy2); - ctx.lineTo(0, -heightBy2); - ctx.lineTo(widthBy2, heightBy2); - ctx.closePath(); - - this._renderPaintInOrder(ctx); - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - var widthBy2 = this.width / 2, - heightBy2 = this.height / 2, - points = [ - -widthBy2 + ' ' + heightBy2, - '0 ' + -heightBy2, - widthBy2 + ' ' + heightBy2 - ].join(','); - return [ - '' - ]; - }, - /* _TO_SVG_END_ */ - }); - - /** - * Returns {@link fabric.Triangle} instance from an object representation - * @static - * @memberOf fabric.Triangle - * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument - */ - fabric.Triangle.fromObject = function (object, callback) { - return fabric.Object._fromObject('Triangle', object, callback); - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - piBy2 = Math.PI * 2; - - if (fabric.Ellipse) { - fabric.warn('fabric.Ellipse is already defined.'); - return; - } - - /** - * Ellipse class - * @class fabric.Ellipse - * @extends fabric.Object - * @return {fabric.Ellipse} thisArg - * @see {@link fabric.Ellipse#initialize} for constructor definition - */ - fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'ellipse', - - /** - * Horizontal radius - * @type Number - * @default - */ - rx: 0, - - /** - * Vertical radius - * @type Number - * @default - */ - ry: 0, - - cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), - - /** - * Constructor - * @param {Object} [options] Options object - * @return {fabric.Ellipse} thisArg - */ - initialize: function (options) { - this.callSuper('initialize', options); - this.set('rx', options && options.rx || 0); - this.set('ry', options && options.ry || 0); - }, - - /** - * @private - * @param {String} key - * @param {*} value - * @return {fabric.Ellipse} thisArg - */ - _set: function (key, value) { - this.callSuper('_set', key, value); - switch (key) { - - case 'rx': - this.rx = value; - this.set('width', value * 2); - break; - - case 'ry': - this.ry = value; - this.set('height', value * 2); - break; - - } - return this; - }, - - /** - * Returns horizontal radius of an object (according to how an object is scaled) - * @return {Number} - */ - getRx: function () { - return this.get('rx') * this.get('scaleX'); - }, - - /** - * Returns Vertical radius of an object (according to how an object is scaled) - * @return {Number} - */ - getRy: function () { - return this.get('ry') * this.get('scaleY'); - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - return [ - '\n' - ]; - }, - /* _TO_SVG_END_ */ - - /** - * @private - * @param {CanvasRenderingContext2D} ctx context to render on - */ - _render: function (ctx) { - ctx.beginPath(); - ctx.save(); - ctx.transform(1, 0, 0, this.ry / this.rx, 0, 0); - ctx.arc( - 0, - 0, - this.rx, - 0, - piBy2, - false); - ctx.restore(); - this._renderPaintInOrder(ctx); - }, - }); - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) - * @static - * @memberOf fabric.Ellipse - * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement - */ - fabric.Ellipse.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy rx ry'.split(' ')); - - /** - * Returns {@link fabric.Ellipse} instance from an SVG element - * @static - * @memberOf fabric.Ellipse - * @param {SVGElement} element Element to parse - * @param {Function} [callback] Options callback invoked after parsing is finished - * @return {fabric.Ellipse} - */ - fabric.Ellipse.fromElement = function (element, callback) { - - var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); - - parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.rx; - parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.ry; - callback(new fabric.Ellipse(parsedAttributes)); - }; - /* _FROM_SVG_END_ */ - - /** - * Returns {@link fabric.Ellipse} instance from an object representation - * @static - * @memberOf fabric.Ellipse - * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument - * @return {void} - */ - fabric.Ellipse.fromObject = function (object, callback) { - fabric.Object._fromObject('Ellipse', object, callback); - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - extend = fabric.util.object.extend; - - if (fabric.Rect) { - fabric.warn('fabric.Rect is already defined'); - return; - } - - /** - * Rectangle class - * @class fabric.Rect - * @extends fabric.Object - * @return {fabric.Rect} thisArg - * @see {@link fabric.Rect#initialize} for constructor definition - */ - fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ { - - /** - * List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged}) - * as well as for history (undo/redo) purposes - * @type Array - */ - stateProperties: fabric.Object.prototype.stateProperties.concat('rx', 'ry'), - - /** - * Type of an object - * @type String - * @default - */ - type: 'rect', - - /** - * Horizontal border radius - * @type Number - * @default - */ - rx: 0, - - /** - * Vertical border radius - * @type Number - * @default - */ - ry: 0, - - cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), - - /** - * Constructor - * @param {Object} [options] Options object - * @return {Object} thisArg - */ - initialize: function (options) { - this.callSuper('initialize', options); - this._initRxRy(); - }, - - /** - * Initializes rx/ry attributes - * @private - */ - _initRxRy: function () { - if (this.rx && !this.ry) { - this.ry = this.rx; - } - else if (this.ry && !this.rx) { - this.rx = this.ry; - } - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - - // 1x1 case (used in spray brush) optimization was removed because - // with caching and higher zoom level this makes more damage than help - - var rx = this.rx ? Math.min(this.rx, this.width / 2) : 0, - ry = this.ry ? Math.min(this.ry, this.height / 2) : 0, - w = this.width, - h = this.height, - x = -this.width / 2, - y = -this.height / 2, - isRounded = rx !== 0 || ry !== 0, - /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */ - k = 1 - 0.5522847498; - ctx.beginPath(); - - ctx.moveTo(x + rx, y); - - ctx.lineTo(x + w - rx, y); - isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry); - - ctx.lineTo(x + w, y + h - ry); - isRounded && ctx.bezierCurveTo(x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h); - - ctx.lineTo(x + rx, y + h); - isRounded && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry); - - ctx.lineTo(x, y + ry); - isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y); - - ctx.closePath(); - - this._renderPaintInOrder(ctx); - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - var x = -this.width / 2, y = -this.height / 2; - return [ - '\n' - ]; - }, - /* _TO_SVG_END_ */ - }); - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) - * @static - * @memberOf fabric.Rect - * @see: http://www.w3.org/TR/SVG/shapes.html#RectElement - */ - fabric.Rect.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y rx ry width height'.split(' ')); - - /** - * Returns {@link fabric.Rect} instance from an SVG element - * @static - * @memberOf fabric.Rect - * @param {SVGElement} element Element to parse - * @param {Function} callback callback function invoked after parsing - * @param {Object} [options] Options object - */ - fabric.Rect.fromElement = function (element, callback, options) { - if (!element) { - return callback(null); - } - options = options || {}; - - var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES); - parsedAttributes.left = parsedAttributes.left || 0; - parsedAttributes.top = parsedAttributes.top || 0; - parsedAttributes.height = parsedAttributes.height || 0; - parsedAttributes.width = parsedAttributes.width || 0; - var rect = new fabric.Rect(extend((options ? fabric.util.object.clone(options) : {}), parsedAttributes)); - rect.visible = rect.visible && rect.width > 0 && rect.height > 0; - callback(rect); - }; - /* _FROM_SVG_END_ */ - - /** - * Returns {@link fabric.Rect} instance from an object representation - * @static - * @memberOf fabric.Rect - * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created - */ - fabric.Rect.fromObject = function (object, callback) { - return fabric.Object._fromObject('Rect', object, callback); - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - extend = fabric.util.object.extend, - min = fabric.util.array.min, - max = fabric.util.array.max, - toFixed = fabric.util.toFixed, - projectStrokeOnPoints = fabric.util.projectStrokeOnPoints; - - if (fabric.Polyline) { - fabric.warn('fabric.Polyline is already defined'); - return; - } - - /** - * Polyline class - * @class fabric.Polyline - * @extends fabric.Object - * @see {@link fabric.Polyline#initialize} for constructor definition - */ - fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'polyline', - - /** - * Points array - * @type Array - * @default - */ - points: null, - - /** - * WARNING: Feature in progress - * Calculate the exact bounding box taking in account strokeWidth on acute angles - * this will be turned to true by default on fabric 6.0 - * maybe will be left in as an optimization since calculations may be slow - * @deprecated - * @type Boolean - * @default false - */ - exactBoundingBox: false, - - cacheProperties: fabric.Object.prototype.cacheProperties.concat('points'), - - /** - * Constructor - * @param {Array} points Array of points (where each point is an object with x and y) - * @param {Object} [options] Options object - * @return {fabric.Polyline} thisArg - * @example - * var poly = new fabric.Polyline([ - * { x: 10, y: 10 }, - * { x: 50, y: 30 }, - * { x: 40, y: 70 }, - * { x: 60, y: 50 }, - * { x: 100, y: 150 }, - * { x: 40, y: 100 } - * ], { - * stroke: 'red', - * left: 100, - * top: 100 - * }); - */ - initialize: function (points, options) { - options = options || {}; - this.points = points || []; - this.callSuper('initialize', options); - this._setPositionDimensions(options); - }, - - /** - * @private - */ - _projectStrokeOnPoints: function () { - return projectStrokeOnPoints(this.points, this, true); - }, - - _setPositionDimensions: function (options) { - var calcDim = this._calcDimensions(options), correctLeftTop, - correctSize = this.exactBoundingBox ? this.strokeWidth : 0; - this.width = calcDim.width - correctSize; - this.height = calcDim.height - correctSize; - if (!options.fromSVG) { - correctLeftTop = this.translateToGivenOrigin( - { - // this looks bad, but is one way to keep it optional for now. - x: calcDim.left - this.strokeWidth / 2 + correctSize / 2, - y: calcDim.top - this.strokeWidth / 2 + correctSize / 2 - }, - 'left', - 'top', - this.originX, - this.originY - ); - } - if (typeof options.left === 'undefined') { - this.left = options.fromSVG ? calcDim.left : correctLeftTop.x; - } - if (typeof options.top === 'undefined') { - this.top = options.fromSVG ? calcDim.top : correctLeftTop.y; - } - this.pathOffset = { - x: calcDim.left + this.width / 2 + correctSize / 2, - y: calcDim.top + this.height / 2 + correctSize / 2 - }; - }, - - /** - * Calculate the polygon min and max point from points array, - * returning an object with left, top, width, height to measure the - * polygon size - * @return {Object} object.left X coordinate of the polygon leftmost point - * @return {Object} object.top Y coordinate of the polygon topmost point - * @return {Object} object.width distance between X coordinates of the polygon leftmost and rightmost point - * @return {Object} object.height distance between Y coordinates of the polygon topmost and bottommost point - * @private - */ - _calcDimensions: function () { - - var points = this.exactBoundingBox ? this._projectStrokeOnPoints() : this.points, - minX = min(points, 'x') || 0, - minY = min(points, 'y') || 0, - maxX = max(points, 'x') || 0, - maxY = max(points, 'y') || 0, - width = (maxX - minX), - height = (maxY - minY); - - return { - left: minX, - top: minY, - width: width, - height: height, - }; - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of an instance - */ - toObject: function (propertiesToInclude) { - return extend(this.callSuper('toObject', propertiesToInclude), { - points: this.points.concat() - }); - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - var points = [], diffX = this.pathOffset.x, diffY = this.pathOffset.y, - NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - - for (var i = 0, len = this.points.length; i < len; i++) { - points.push( - toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', - toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ' - ); - } - return [ - '<' + this.type + ' ', 'COMMON_PARTS', - 'points="', points.join(''), - '" />\n' - ]; - }, - /* _TO_SVG_END_ */ - - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - commonRender: function (ctx) { - var point, len = this.points.length, - x = this.pathOffset.x, - y = this.pathOffset.y; - - if (!len || isNaN(this.points[len - 1].y)) { - // do not draw if no points or odd points - // NaN comes from parseFloat of a empty string in parser - return false; - } - ctx.beginPath(); - ctx.moveTo(this.points[0].x - x, this.points[0].y - y); - for (var i = 0; i < len; i++) { - point = this.points[i]; - ctx.lineTo(point.x - x, point.y - y); - } - return true; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - if (!this.commonRender(ctx)) { - return; - } - this._renderPaintInOrder(ctx); - }, - - /** - * Returns complexity of an instance - * @return {Number} complexity of this instance - */ - complexity: function () { - return this.get('points').length; - } - }); - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Polyline.fromElement}) - * @static - * @memberOf fabric.Polyline - * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement - */ - fabric.Polyline.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); - - /** - * Returns fabric.Polyline instance from an SVG element - * @static - * @memberOf fabric.Polyline - * @param {SVGElement} element Element to parser - * @param {Function} callback callback function invoked after parsing - * @param {Object} [options] Options object - */ - fabric.Polyline.fromElementGenerator = function (_class) { - return function (element, callback, options) { - if (!element) { - return callback(null); - } - options || (options = {}); - - var points = fabric.parsePointsAttribute(element.getAttribute('points')), - parsedAttributes = fabric.parseAttributes(element, fabric[_class].ATTRIBUTE_NAMES); - parsedAttributes.fromSVG = true; - callback(new fabric[_class](points, extend(parsedAttributes, options))); - }; - }; - - fabric.Polyline.fromElement = fabric.Polyline.fromElementGenerator('Polyline'); - - /* _FROM_SVG_END_ */ - - /** - * Returns fabric.Polyline instance from an object representation - * @static - * @memberOf fabric.Polyline - * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created - */ - fabric.Polyline.fromObject = function (object, callback) { - return fabric.Object._fromObject('Polyline', object, callback, 'points'); - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - projectStrokeOnPoints = fabric.util.projectStrokeOnPoints; - - if (fabric.Polygon) { - fabric.warn('fabric.Polygon is already defined'); - return; - } - - /** - * Polygon class - * @class fabric.Polygon - * @extends fabric.Polyline - * @see {@link fabric.Polygon#initialize} for constructor definition - */ - fabric.Polygon = fabric.util.createClass(fabric.Polyline, /** @lends fabric.Polygon.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'polygon', - - /** - * @private - */ - _projectStrokeOnPoints: function () { - return projectStrokeOnPoints(this.points, this); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - if (!this.commonRender(ctx)) { - return; - } - ctx.closePath(); - this._renderPaintInOrder(ctx); - }, - - }); - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) - * @static - * @memberOf fabric.Polygon - * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement - */ - fabric.Polygon.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); - - /** - * Returns {@link fabric.Polygon} instance from an SVG element - * @static - * @memberOf fabric.Polygon - * @param {SVGElement} element Element to parse - * @param {Function} callback callback function invoked after parsing - * @param {Object} [options] Options object - */ - fabric.Polygon.fromElement = fabric.Polyline.fromElementGenerator('Polygon'); - /* _FROM_SVG_END_ */ - - /** - * Returns fabric.Polygon instance from an object representation - * @static - * @memberOf fabric.Polygon - * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created - * @return {void} - */ - fabric.Polygon.fromObject = function (object, callback) { - fabric.Object._fromObject('Polygon', object, callback, 'points'); - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - min = fabric.util.array.min, - max = fabric.util.array.max, - extend = fabric.util.object.extend, - clone = fabric.util.object.clone, - toFixed = fabric.util.toFixed; - - if (fabric.Path) { - fabric.warn('fabric.Path is already defined'); - return; - } - - /** - * Path class - * @class fabric.Path - * @extends fabric.Object - * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#path_and_pathgroup} - * @see {@link fabric.Path#initialize} for constructor definition - */ - fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'path', - - /** - * Array of path points - * @type Array - * @default - */ - path: null, - - cacheProperties: fabric.Object.prototype.cacheProperties.concat('path', 'fillRule'), - - stateProperties: fabric.Object.prototype.stateProperties.concat('path'), - - /** - * Constructor - * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) - * @param {Object} [options] Options object - * @return {fabric.Path} thisArg - */ - initialize: function (path, options) { - options = clone(options || {}); - delete options.path; - this.callSuper('initialize', options); - this._setPath(path || [], options); - }, - - /** - * @private - * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) - * @param {Object} [options] Options object - */ - _setPath: function (path, options) { - this.path = fabric.util.makePathSimpler( - Array.isArray(path) ? path : fabric.util.parsePath(path) - ); - - fabric.Polyline.prototype._setPositionDimensions.call(this, options || {}); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx context to render path on - */ - _renderPathCommands: function (ctx) { - var current, // current instruction - subpathStartX = 0, - subpathStartY = 0, - x = 0, // current x - y = 0, // current y - controlX = 0, // current control point x - controlY = 0, // current control point y - l = -this.pathOffset.x, - t = -this.pathOffset.y; - - ctx.beginPath(); - - for (var i = 0, len = this.path.length; i < len; ++i) { - - current = this.path[i]; - - switch (current[0]) { // first letter - - case 'L': // lineto, absolute - x = current[1]; - y = current[2]; - ctx.lineTo(x + l, y + t); - break; - - case 'M': // moveTo, absolute - x = current[1]; - y = current[2]; - subpathStartX = x; - subpathStartY = y; - ctx.moveTo(x + l, y + t); - break; - - case 'C': // bezierCurveTo, absolute - x = current[5]; - y = current[6]; - controlX = current[3]; - controlY = current[4]; - ctx.bezierCurveTo( - current[1] + l, - current[2] + t, - controlX + l, - controlY + t, - x + l, - y + t - ); - break; - - case 'Q': // quadraticCurveTo, absolute - ctx.quadraticCurveTo( - current[1] + l, - current[2] + t, - current[3] + l, - current[4] + t - ); - x = current[3]; - y = current[4]; - controlX = current[1]; - controlY = current[2]; - break; - - case 'z': - case 'Z': - x = subpathStartX; - y = subpathStartY; - ctx.closePath(); - break; - } - } - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx context to render path on - */ - _render: function (ctx) { - this._renderPathCommands(ctx); - this._renderPaintInOrder(ctx); - }, - - /** - * Returns string representation of an instance - * @return {String} string representation of an instance - */ - toString: function () { - return '#'; - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - return extend(this.callSuper('toObject', propertiesToInclude), { - path: this.path.map(function (item) { return item.slice(); }), - }); - }, - - /** - * Returns dataless object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toDatalessObject: function (propertiesToInclude) { - var o = this.toObject(['sourcePath'].concat(propertiesToInclude)); - if (o.sourcePath) { - delete o.path; - } - return o; - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - var path = fabric.util.joinPath(this.path); - return [ - '\n' - ]; - }, - - _getOffsetTransform: function () { - var digits = fabric.Object.NUM_FRACTION_DIGITS; - return ' translate(' + toFixed(-this.pathOffset.x, digits) + ', ' + - toFixed(-this.pathOffset.y, digits) + ')'; - }, - - /** - * Returns svg clipPath representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - toClipPathSVG: function (reviver) { - var additionalTransform = this._getOffsetTransform(); - return '\t' + this._createBaseClipPathSVGMarkup( - this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform } - ); - }, - - /** - * Returns svg representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - toSVG: function (reviver) { - var additionalTransform = this._getOffsetTransform(); - return this._createBaseSVGMarkup(this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform }); - }, - /* _TO_SVG_END_ */ - - /** - * Returns number representation of an instance complexity - * @return {Number} complexity of this instance - */ - complexity: function () { - return this.path.length; - }, - - /** - * @private - */ - _calcDimensions: function () { - - var aX = [], - aY = [], - current, // current instruction - subpathStartX = 0, - subpathStartY = 0, - x = 0, // current x - y = 0, // current y - bounds; - - for (var i = 0, len = this.path.length; i < len; ++i) { - - current = this.path[i]; - - switch (current[0]) { // first letter - - case 'L': // lineto, absolute - x = current[1]; - y = current[2]; - bounds = []; - break; - - case 'M': // moveTo, absolute - x = current[1]; - y = current[2]; - subpathStartX = x; - subpathStartY = y; - bounds = []; - break; - - case 'C': // bezierCurveTo, absolute - bounds = fabric.util.getBoundsOfCurve(x, y, - current[1], - current[2], - current[3], - current[4], - current[5], - current[6] - ); - x = current[5]; - y = current[6]; - break; - - case 'Q': // quadraticCurveTo, absolute - bounds = fabric.util.getBoundsOfCurve(x, y, - current[1], - current[2], - current[1], - current[2], - current[3], - current[4] - ); - x = current[3]; - y = current[4]; - break; - - case 'z': - case 'Z': - x = subpathStartX; - y = subpathStartY; - break; - } - bounds.forEach(function (point) { - aX.push(point.x); - aY.push(point.y); - }); - aX.push(x); - aY.push(y); - } - - var minX = min(aX) || 0, - minY = min(aY) || 0, - maxX = max(aX) || 0, - maxY = max(aY) || 0, - deltaX = maxX - minX, - deltaY = maxY - minY; - - return { - left: minX, - top: minY, - width: deltaX, - height: deltaY - }; - } - }); - - /** - * Creates an instance of fabric.Path from an object - * @static - * @memberOf fabric.Path - * @param {Object} object - * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created - */ - fabric.Path.fromObject = function (object, callback) { - if (typeof object.sourcePath === 'string') { - var pathUrl = object.sourcePath; - fabric.loadSVGFromURL(pathUrl, function (elements) { - var path = elements[0]; - path.setOptions(object); - if (object.clipPath) { - fabric.util.enlivenObjects([object.clipPath], function (elivenedObjects) { - path.clipPath = elivenedObjects[0]; - callback && callback(path); - }); - } - else { - callback && callback(path); - } - }); - } - else { - fabric.Object._fromObject('Path', object, callback, 'path'); - } - }; - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) - * @static - * @memberOf fabric.Path - * @see http://www.w3.org/TR/SVG/paths.html#PathElement - */ - fabric.Path.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(['d']); - - /** - * Creates an instance of fabric.Path from an SVG element - * @static - * @memberOf fabric.Path - * @param {SVGElement} element to parse - * @param {Function} callback Callback to invoke when an fabric.Path instance is created - * @param {Object} [options] Options object - * @param {Function} [callback] Options callback invoked after parsing is finished - */ - fabric.Path.fromElement = function (element, callback, options) { - var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES); - parsedAttributes.fromSVG = true; - callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); - }; - /* _FROM_SVG_END_ */ - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - min = fabric.util.array.min, - max = fabric.util.array.max; - - if (fabric.Group) { - return; - } - - /** - * Group class - * @class fabric.Group - * @extends fabric.Object - * @mixes fabric.Collection - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} - * @see {@link fabric.Group#initialize} for constructor definition - */ - fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'group', - - /** - * Width of stroke - * @type Number - * @default - */ - strokeWidth: 0, - - /** - * Indicates if click, mouseover, mouseout events & hoverCursor should also check for subtargets - * @type Boolean - * @default - */ - subTargetCheck: false, - - /** - * Groups are container, do not render anything on theyr own, ence no cache properties - * @type Array - * @default - */ - cacheProperties: [], - - /** - * setOnGroup is a method used for TextBox that is no more used since 2.0.0 The behavior is still - * available setting this boolean to true. - * @type Boolean - * @since 2.0.0 - * @default - */ - useSetOnGroup: false, - - /** - * Constructor - * @param {Object} objects Group objects - * @param {Object} [options] Options object - * @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already. - * @return {Object} thisArg - */ - initialize: function (objects, options, isAlreadyGrouped) { - options = options || {}; - this._objects = []; - // if objects enclosed in a group have been grouped already, - // we cannot change properties of objects. - // Thus we need to set options to group without objects, - isAlreadyGrouped && this.callSuper('initialize', options); - this._objects = objects || []; - for (var i = this._objects.length; i--;) { - this._objects[i].group = this; - } - - if (!isAlreadyGrouped) { - var center = options && options.centerPoint; - // we want to set origins before calculating the bounding box. - // so that the topleft can be set with that in mind. - // if specific top and left are passed, are overwritten later - // with the callSuper('initialize', options) - if (options.originX !== undefined) { - this.originX = options.originX; - } - if (options.originY !== undefined) { - this.originY = options.originY; - } - // if coming from svg i do not want to calc bounds. - // i assume width and height are passed along options - center || this._calcBounds(); - this._updateObjectsCoords(center); - delete options.centerPoint; - this.callSuper('initialize', options); - } - else { - this._updateObjectsACoords(); - } - - this.setCoords(); - }, - - /** - * @private - */ - _updateObjectsACoords: function () { - var skipControls = true; - for (var i = this._objects.length; i--;) { - this._objects[i].setCoords(skipControls); - } - }, - - /** - * @private - * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change - */ - _updateObjectsCoords: function (center) { - var center = center || this.getCenterPoint(); - for (var i = this._objects.length; i--;) { - this._updateObjectCoords(this._objects[i], center); - } - }, - - /** - * @private - * @param {Object} object - * @param {fabric.Point} center, current center of group. - */ - _updateObjectCoords: function (object, center) { - var objectLeft = object.left, - objectTop = object.top, - skipControls = true; - - object.set({ - left: objectLeft - center.x, - top: objectTop - center.y - }); - object.group = this; - object.setCoords(skipControls); - }, - - /** - * Returns string represenation of a group - * @return {String} - */ - toString: function () { - return '#'; - }, - - /** - * Adds an object to a group; Then recalculates group's dimension, position. - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - addWithUpdate: function (object) { - var nested = !!this.group; - this._restoreObjectsState(); - fabric.util.resetObjectTransform(this); - if (object) { - if (nested) { - // if this group is inside another group, we need to pre transform the object - fabric.util.removeTransformFromObject(object, this.group.calcTransformMatrix()); - } - this._objects.push(object); - object.group = this; - object._set('canvas', this.canvas); - } - this._calcBounds(); - this._updateObjectsCoords(); - this.dirty = true; - if (nested) { - this.group.addWithUpdate(); - } - else { - this.setCoords(); - } - return this; - }, - - /** - * Removes an object from a group; Then recalculates group's dimension, position. - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - removeWithUpdate: function (object) { - this._restoreObjectsState(); - fabric.util.resetObjectTransform(this); - - this.remove(object); - this._calcBounds(); - this._updateObjectsCoords(); - this.setCoords(); - this.dirty = true; - return this; - }, - - /** - * @private - */ - _onObjectAdded: function (object) { - this.dirty = true; - object.group = this; - object._set('canvas', this.canvas); - }, - - /** - * @private - */ - _onObjectRemoved: function (object) { - this.dirty = true; - delete object.group; - }, - - /** - * @private - */ - _set: function (key, value) { - var i = this._objects.length; - if (this.useSetOnGroup) { - while (i--) { - this._objects[i].setOnGroup(key, value); - } - } - if (key === 'canvas') { - while (i--) { - this._objects[i]._set(key, value); - } - } - fabric.Object.prototype._set.call(this, key, value); - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - var _includeDefaultValues = this.includeDefaultValues; - var objsToObject = this._objects - .filter(function (obj) { - return !obj.excludeFromExport; - }) - .map(function (obj) { - var originalDefaults = obj.includeDefaultValues; - obj.includeDefaultValues = _includeDefaultValues; - var _obj = obj.toObject(propertiesToInclude); - obj.includeDefaultValues = originalDefaults; - return _obj; - }); - var obj = fabric.Object.prototype.toObject.call(this, propertiesToInclude); - obj.objects = objsToObject; - return obj; - }, - - /** - * Returns object representation of an instance, in dataless mode. - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toDatalessObject: function (propertiesToInclude) { - var objsToObject, sourcePath = this.sourcePath; - if (sourcePath) { - objsToObject = sourcePath; - } - else { - var _includeDefaultValues = this.includeDefaultValues; - objsToObject = this._objects.map(function (obj) { - var originalDefaults = obj.includeDefaultValues; - obj.includeDefaultValues = _includeDefaultValues; - var _obj = obj.toDatalessObject(propertiesToInclude); - obj.includeDefaultValues = originalDefaults; - return _obj; - }); - } - var obj = fabric.Object.prototype.toDatalessObject.call(this, propertiesToInclude); - obj.objects = objsToObject; - return obj; - }, - - /** - * Renders instance on a given context - * @param {CanvasRenderingContext2D} ctx context to render instance on - */ - render: function (ctx) { - this._transformDone = true; - this.callSuper('render', ctx); - this._transformDone = false; - }, - - /** - * Decide if the object should cache or not. Create its own cache level - * needsItsOwnCache should be used when the object drawing method requires - * a cache step. None of the fabric classes requires it. - * Generally you do not cache objects in groups because the group is already cached. - * @return {Boolean} - */ - shouldCache: function () { - var ownCache = fabric.Object.prototype.shouldCache.call(this); - if (ownCache) { - for (var i = 0, len = this._objects.length; i < len; i++) { - if (this._objects[i].willDrawShadow()) { - this.ownCaching = false; - return false; - } - } - } - return ownCache; - }, - - /** - * Check if this object or a child object will cast a shadow - * @return {Boolean} - */ - willDrawShadow: function () { - if (fabric.Object.prototype.willDrawShadow.call(this)) { - return true; - } - for (var i = 0, len = this._objects.length; i < len; i++) { - if (this._objects[i].willDrawShadow()) { - return true; - } - } - return false; - }, - - /** - * Check if this group or its parent group are caching, recursively up - * @return {Boolean} - */ - isOnACache: function () { - return this.ownCaching || (this.group && this.group.isOnACache()); - }, - - /** - * Execute the drawing operation for an object on a specified context - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - drawObject: function (ctx) { - for (var i = 0, len = this._objects.length; i < len; i++) { - this._objects[i].render(ctx); - } - this._drawClipPath(ctx, this.clipPath); - }, - - /** - * Check if cache is dirty - */ - isCacheDirty: function (skipCanvas) { - if (this.callSuper('isCacheDirty', skipCanvas)) { - return true; - } - if (!this.statefullCache) { - return false; - } - for (var i = 0, len = this._objects.length; i < len; i++) { - if (this._objects[i].isCacheDirty(true)) { - if (this._cacheCanvas) { - // if this group has not a cache canvas there is nothing to clean - var x = this.cacheWidth / this.zoomX, y = this.cacheHeight / this.zoomY; - this._cacheContext.clearRect(-x / 2, -y / 2, x, y); - } - return true; - } - } - return false; - }, - - /** - * Restores original state of each of group objects (original state is that which was before group was created). - * if the nested boolean is true, the original state will be restored just for the - * first group and not for all the group chain - * @private - * @param {Boolean} nested tell the function to restore object state up to the parent group and not more - * @return {fabric.Group} thisArg - * @chainable - */ - _restoreObjectsState: function () { - var groupMatrix = this.calcOwnMatrix(); - this._objects.forEach(function (object) { - // instead of using _this = this; - fabric.util.addTransformToObject(object, groupMatrix); - delete object.group; - object.setCoords(); - }); - return this; - }, - - /** - * Destroys a group (restoring state of its objects) - * @return {fabric.Group} thisArg - * @chainable - */ - destroy: function () { - // when group is destroyed objects needs to get a repaint to be eventually - // displayed on canvas. - this._objects.forEach(function (object) { - object.set('dirty', true); - }); - return this._restoreObjectsState(); - }, - - dispose: function () { - this.callSuper('dispose'); - this.forEachObject(function (object) { - object.dispose && object.dispose(); - }); - this._objects = []; - }, - - /** - * make a group an active selection, remove the group from canvas - * the group has to be on canvas for this to work. - * @return {fabric.ActiveSelection} thisArg - * @chainable - */ - toActiveSelection: function () { - if (!this.canvas) { - return; - } - var objects = this._objects, canvas = this.canvas; - this._objects = []; - var options = this.toObject(); - delete options.objects; - var activeSelection = new fabric.ActiveSelection([]); - activeSelection.set(options); - activeSelection.type = 'activeSelection'; - canvas.remove(this); - objects.forEach(function (object) { - object.group = activeSelection; - object.dirty = true; - canvas.add(object); - }); - activeSelection.canvas = canvas; - activeSelection._objects = objects; - canvas._activeObject = activeSelection; - activeSelection.setCoords(); - return activeSelection; - }, - - /** - * Destroys a group (restoring state of its objects) - * @return {fabric.Group} thisArg - * @chainable - */ - ungroupOnCanvas: function () { - return this._restoreObjectsState(); - }, - - /** - * Sets coordinates of all objects inside group - * @return {fabric.Group} thisArg - * @chainable - */ - setObjectsCoords: function () { - var skipControls = true; - this.forEachObject(function (object) { - object.setCoords(skipControls); - }); - return this; - }, - - /** - * @private - */ - _calcBounds: function (onlyWidthHeight) { - var aX = [], - aY = [], - o, prop, coords, - props = ['tr', 'br', 'bl', 'tl'], - i = 0, iLen = this._objects.length, - j, jLen = props.length; - - for (; i < iLen; ++i) { - o = this._objects[i]; - coords = o.calcACoords(); - for (j = 0; j < jLen; j++) { - prop = props[j]; - aX.push(coords[prop].x); - aY.push(coords[prop].y); - } - o.aCoords = coords; - } - - this._getBounds(aX, aY, onlyWidthHeight); - }, - - /** - * @private - */ - _getBounds: function (aX, aY, onlyWidthHeight) { - var minXY = new fabric.Point(min(aX), min(aY)), - maxXY = new fabric.Point(max(aX), max(aY)), - top = minXY.y || 0, left = minXY.x || 0, - width = (maxXY.x - minXY.x) || 0, - height = (maxXY.y - minXY.y) || 0; - this.width = width; - this.height = height; - if (!onlyWidthHeight) { - // the bounding box always finds the topleft most corner. - // whatever is the group origin, we set up here the left/top position. - this.setPositionByOrigin({ x: left, y: top }, 'left', 'top'); - } - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - _toSVG: function (reviver) { - var svgString = ['\n']; - - for (var i = 0, len = this._objects.length; i < len; i++) { - svgString.push('\t\t', this._objects[i].toSVG(reviver)); - } - svgString.push('\n'); - return svgString; - }, - - /** - * Returns styles-string for svg-export, specific version for group - * @return {String} - */ - getSvgStyles: function () { - var opacity = typeof this.opacity !== 'undefined' && this.opacity !== 1 ? - 'opacity: ' + this.opacity + ';' : '', - visibility = this.visible ? '' : ' visibility: hidden;'; - return [ - opacity, - this.getSvgFilter(), - visibility - ].join(''); - }, - - /** - * Returns svg clipPath representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - toClipPathSVG: function (reviver) { - var svgString = []; - - for (var i = 0, len = this._objects.length; i < len; i++) { - svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); - } - - return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); - }, - /* _TO_SVG_END_ */ - }); - - /** - * Returns {@link fabric.Group} instance from an object representation - * @static - * @memberOf fabric.Group - * @param {Object} object Object to create a group from - * @param {Function} [callback] Callback to invoke when an group instance is created - */ - fabric.Group.fromObject = function (object, callback) { - var objects = object.objects, - options = fabric.util.object.clone(object, true); - delete options.objects; - if (typeof objects === 'string') { - // it has to be an url or something went wrong. - fabric.loadSVGFromURL(objects, function (elements) { - var group = fabric.util.groupSVGElements(elements, object, objects); - var clipPath = options.clipPath; - delete options.clipPath; - group.set(options); - if (clipPath) { - fabric.util.enlivenObjects([clipPath], function (elivenedObjects) { - group.clipPath = elivenedObjects[0]; - callback && callback(group); - }); - } - else { - callback && callback(group); - } - }); - return; - } - fabric.util.enlivenObjects(objects, function (enlivenedObjects) { - fabric.util.enlivenObjectEnlivables(object, options, function () { - callback && callback(new fabric.Group(enlivenedObjects, options, true)); - }); - }); - }; -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}); - - if (fabric.ActiveSelection) { - return; - } - - /** - * Group class - * @class fabric.ActiveSelection - * @extends fabric.Group - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} - * @see {@link fabric.ActiveSelection#initialize} for constructor definition - */ - fabric.ActiveSelection = fabric.util.createClass(fabric.Group, /** @lends fabric.ActiveSelection.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'activeSelection', - - /** - * Constructor - * @param {Object} objects ActiveSelection objects - * @param {Object} [options] Options object - * @return {Object} thisArg - */ - initialize: function (objects, options) { - options = options || {}; - this._objects = objects || []; - for (var i = this._objects.length; i--;) { - this._objects[i].group = this; - } - - if (options.originX) { - this.originX = options.originX; - } - if (options.originY) { - this.originY = options.originY; - } - this._calcBounds(); - this._updateObjectsCoords(); - fabric.Object.prototype.initialize.call(this, options); - this.setCoords(); - }, - - /** - * Change te activeSelection to a normal group, - * High level function that automatically adds it to canvas as - * active object. no events fired. - * @since 2.0.0 - * @return {fabric.Group} - */ - toGroup: function () { - var objects = this._objects.concat(); - this._objects = []; - var options = fabric.Object.prototype.toObject.call(this); - var newGroup = new fabric.Group([]); - delete options.type; - newGroup.set(options); - objects.forEach(function (object) { - object.canvas.remove(object); - object.group = newGroup; - }); - newGroup._objects = objects; - if (!this.canvas) { - return newGroup; - } - var canvas = this.canvas; - canvas.add(newGroup); - canvas._activeObject = newGroup; - newGroup.setCoords(); - return newGroup; - }, - - /** - * If returns true, deselection is cancelled. - * @since 2.0.0 - * @return {Boolean} [cancel] - */ - onDeselect: function () { - this.destroy(); - return false; - }, - - /** - * Returns string representation of a group - * @return {String} - */ - toString: function () { - return '#'; - }, - - /** - * Decide if the object should cache or not. Create its own cache level - * objectCaching is a global flag, wins over everything - * needsItsOwnCache should be used when the object drawing method requires - * a cache step. None of the fabric classes requires it. - * Generally you do not cache objects in groups because the group outside is cached. - * @return {Boolean} - */ - shouldCache: function () { - return false; - }, - - /** - * Check if this group or its parent group are caching, recursively up - * @return {Boolean} - */ - isOnACache: function () { - return false; - }, - - /** - * Renders controls and borders for the object - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Object} [styleOverride] properties to override the object style - * @param {Object} [childrenOverride] properties to override the children overrides - */ - _renderControls: function (ctx, styleOverride, childrenOverride) { - ctx.save(); - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - this.callSuper('_renderControls', ctx, styleOverride); - childrenOverride = childrenOverride || {}; - if (typeof childrenOverride.hasControls === 'undefined') { - childrenOverride.hasControls = false; - } - childrenOverride.forActiveSelection = true; - for (var i = 0, len = this._objects.length; i < len; i++) { - this._objects[i]._renderControls(ctx, childrenOverride); - } - ctx.restore(); - }, - }); - - /** - * Returns {@link fabric.ActiveSelection} instance from an object representation - * @static - * @memberOf fabric.ActiveSelection - * @param {Object} object Object to create a group from - * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created - */ - fabric.ActiveSelection.fromObject = function (object, callback) { - fabric.util.enlivenObjects(object.objects, function (enlivenedObjects) { - delete object.objects; - callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); - }); - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var extend = fabric.util.object.extend; - - if (!global.fabric) { - global.fabric = {}; - } - - if (global.fabric.Image) { - fabric.warn('fabric.Image is already defined.'); - return; - } - - /** - * Image class - * @class fabric.Image - * @extends fabric.Object - * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} - * @see {@link fabric.Image#initialize} for constructor definition - */ - fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'image', - - /** - * Width of a stroke. - * For image quality a stroke multiple of 2 gives better results. - * @type Number - * @default - */ - strokeWidth: 0, - - /** - * When calling {@link fabric.Image.getSrc}, return value from element src with `element.getAttribute('src')`. - * This allows for relative urls as image src. - * @since 2.7.0 - * @type Boolean - * @default - */ - srcFromAttribute: false, - - /** - * private - * contains last value of scaleX to detect - * if the Image got resized after the last Render - * @type Number - */ - _lastScaleX: 1, - - /** - * private - * contains last value of scaleY to detect - * if the Image got resized after the last Render - * @type Number - */ - _lastScaleY: 1, - - /** - * private - * contains last value of scaling applied by the apply filter chain - * @type Number - */ - _filterScalingX: 1, - - /** - * private - * contains last value of scaling applied by the apply filter chain - * @type Number - */ - _filterScalingY: 1, - - /** - * minimum scale factor under which any resizeFilter is triggered to resize the image - * 0 will disable the automatic resize. 1 will trigger automatically always. - * number bigger than 1 are not implemented yet. - * @type Number - */ - minimumScaleTrigger: 0.5, - - /** - * List of properties to consider when checking if - * state of an object is changed ({@link fabric.Object#hasStateChanged}) - * as well as for history (undo/redo) purposes - * @type Array - */ - stateProperties: fabric.Object.prototype.stateProperties.concat('cropX', 'cropY'), - - /** - * List of properties to consider when checking if cache needs refresh - * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single - * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty - * and refreshed at the next render - * @type Array - */ - cacheProperties: fabric.Object.prototype.cacheProperties.concat('cropX', 'cropY'), - - /** - * key used to retrieve the texture representing this image - * @since 2.0.0 - * @type String - * @default - */ - cacheKey: '', - - /** - * Image crop in pixels from original image size. - * @since 2.0.0 - * @type Number - * @default - */ - cropX: 0, - - /** - * Image crop in pixels from original image size. - * @since 2.0.0 - * @type Number - * @default - */ - cropY: 0, - - /** - * Indicates whether this canvas will use image smoothing when painting this image. - * Also influence if the cacheCanvas for this image uses imageSmoothing - * @since 4.0.0-beta.11 - * @type Boolean - * @default - */ - imageSmoothing: true, - - /** - * Constructor - * Image can be initialized with any canvas drawable or a string. - * The string should be a url and will be loaded as an image. - * Canvas and Image element work out of the box, while videos require extra code to work. - * Please check video element events for seeking. - * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element - * @param {Object} [options] Options object - * @param {function} [callback] callback function to call after eventual filters applied. - * @return {fabric.Image} thisArg - */ - initialize: function (element, options) { - options || (options = {}); - this.filters = []; - this.cacheKey = 'texture' + fabric.Object.__uid++; - this.callSuper('initialize', options); - this._initElement(element, options); - }, - - /** - * Returns image element which this instance if based on - * @return {HTMLImageElement} Image element - */ - getElement: function () { - return this._element || {}; - }, - - /** - * Sets image element for this instance to a specified one. - * If filters defined they are applied to new image. - * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. - * @param {HTMLImageElement} element - * @param {Object} [options] Options object - * @return {fabric.Image} thisArg - * @chainable - */ - setElement: function (element, options) { - this.removeTexture(this.cacheKey); - this.removeTexture(this.cacheKey + '_filtered'); - this._element = element; - this._originalElement = element; - this._initConfig(options); - if (this.filters.length !== 0) { - this.applyFilters(); - } - // resizeFilters work on the already filtered copy. - // we need to apply resizeFilters AFTER normal filters. - // applyResizeFilters is run more often than normal filters - // and is triggered by user interactions rather than dev code - if (this.resizeFilter) { - this.applyResizeFilters(); - } - return this; - }, - - /** - * Delete a single texture if in webgl mode - */ - removeTexture: function (key) { - var backend = fabric.filterBackend; - if (backend && backend.evictCachesForKey) { - backend.evictCachesForKey(key); - } - }, - - /** - * Delete textures, reference to elements and eventually JSDOM cleanup - */ - dispose: function () { - this.callSuper('dispose'); - this.removeTexture(this.cacheKey); - this.removeTexture(this.cacheKey + '_filtered'); - this._cacheContext = undefined; - ['_originalElement', '_element', '_filteredEl', '_cacheCanvas'].forEach((function (element) { - fabric.util.cleanUpJsdomNode(this[element]); - this[element] = undefined; - }).bind(this)); - }, - - /** - * Get the crossOrigin value (of the corresponding image element) - */ - getCrossOrigin: function () { - return this._originalElement && (this._originalElement.crossOrigin || null); - }, - - /** - * Returns original size of an image - * @return {Object} Object with "width" and "height" properties - */ - getOriginalSize: function () { - var element = this.getElement(); - return { - width: element.naturalWidth || element.width, - height: element.naturalHeight || element.height - }; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _stroke: function (ctx) { - if (!this.stroke || this.strokeWidth === 0) { - return; - } - var w = this.width / 2, h = this.height / 2; - ctx.beginPath(); - ctx.moveTo(-w, -h); - ctx.lineTo(w, -h); - ctx.lineTo(w, h); - ctx.lineTo(-w, h); - ctx.lineTo(-w, -h); - ctx.closePath(); - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of an instance - */ - toObject: function (propertiesToInclude) { - var filters = []; - - this.filters.forEach(function (filterObj) { - if (filterObj) { - filters.push(filterObj.toObject()); - } - }); - var object = extend( - this.callSuper( - 'toObject', - ['cropX', 'cropY'].concat(propertiesToInclude) - ), { - src: this.getSrc(), - crossOrigin: this.getCrossOrigin(), - filters: filters, - }); - if (this.resizeFilter) { - object.resizeFilter = this.resizeFilter.toObject(); - } - return object; - }, - - /** - * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,height. - * @return {Boolean} - */ - hasCrop: function () { - return this.cropX || this.cropY || this.width < this._element.width || this.height < this._element.height; - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - var svgString = [], imageMarkup = [], strokeSvg, element = this._element, - x = -this.width / 2, y = -this.height / 2, clipPath = '', imageRendering = ''; - if (!element) { - return []; - } - if (this.hasCrop()) { - var clipPathId = fabric.Object.__uid++; - svgString.push( - '\n', - '\t\n', - '\n' - ); - clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" '; - } - if (!this.imageSmoothing) { - imageRendering = '" image-rendering="optimizeSpeed'; - } - imageMarkup.push('\t\n'); - - if (this.stroke || this.strokeDashArray) { - var origFill = this.fill; - this.fill = null; - strokeSvg = [ - '\t\n' - ]; - this.fill = origFill; - } - if (this.paintFirst !== 'fill') { - svgString = svgString.concat(strokeSvg, imageMarkup); - } - else { - svgString = svgString.concat(imageMarkup, strokeSvg); - } - return svgString; - }, - /* _TO_SVG_END_ */ - - /** - * Returns source of an image - * @param {Boolean} filtered indicates if the src is needed for svg - * @return {String} Source of an image - */ - getSrc: function (filtered) { - var element = filtered ? this._element : this._originalElement; - if (element) { - if (element.toDataURL) { - return element.toDataURL(); - } - - if (this.srcFromAttribute) { - return element.getAttribute('src'); - } - else { - return element.src; - } - } - else { - return this.src || ''; - } - }, - - /** - * Sets source of an image - * @param {String} src Source string (URL) - * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) - * @param {Object} [options] Options object - * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") - * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes - * @return {fabric.Image} thisArg - * @chainable - */ - setSrc: function (src, callback, options) { - fabric.util.loadImage(src, function (img, isError) { - this.setElement(img, options); - this._setWidthHeight(); - callback && callback(this, isError); - }, this, options && options.crossOrigin); - return this; - }, - - /** - * Returns string representation of an instance - * @return {String} String representation of an instance - */ - toString: function () { - return '#'; - }, - - applyResizeFilters: function () { - var filter = this.resizeFilter, - minimumScale = this.minimumScaleTrigger, - objectScale = this.getTotalObjectScaling(), - scaleX = objectScale.scaleX, - scaleY = objectScale.scaleY, - elementToFilter = this._filteredEl || this._originalElement; - if (this.group) { - this.set('dirty', true); - } - if (!filter || (scaleX > minimumScale && scaleY > minimumScale)) { - this._element = elementToFilter; - this._filterScalingX = 1; - this._filterScalingY = 1; - this._lastScaleX = scaleX; - this._lastScaleY = scaleY; - return; - } - if (!fabric.filterBackend) { - fabric.filterBackend = fabric.initFilterBackend(); - } - var canvasEl = fabric.util.createCanvasElement(), - cacheKey = this._filteredEl ? (this.cacheKey + '_filtered') : this.cacheKey, - sourceWidth = elementToFilter.width, sourceHeight = elementToFilter.height; - canvasEl.width = sourceWidth; - canvasEl.height = sourceHeight; - this._element = canvasEl; - this._lastScaleX = filter.scaleX = scaleX; - this._lastScaleY = filter.scaleY = scaleY; - fabric.filterBackend.applyFilters( - [filter], elementToFilter, sourceWidth, sourceHeight, this._element, cacheKey); - this._filterScalingX = canvasEl.width / this._originalElement.width; - this._filterScalingY = canvasEl.height / this._originalElement.height; - }, - - /** - * Applies filters assigned to this image (from "filters" array) or from filter param - * @method applyFilters - * @param {Array} filters to be applied - * @param {Boolean} forResizing specify if the filter operation is a resize operation - * @return {thisArg} return the fabric.Image object - * @chainable - */ - applyFilters: function (filters) { - - filters = filters || this.filters || []; - filters = filters.filter(function (filter) { return filter && !filter.isNeutralState(); }); - this.set('dirty', true); - - // needs to clear out or WEBGL will not resize correctly - this.removeTexture(this.cacheKey + '_filtered'); - - if (filters.length === 0) { - this._element = this._originalElement; - this._filteredEl = null; - this._filterScalingX = 1; - this._filterScalingY = 1; - return this; - } - - var imgElement = this._originalElement, - sourceWidth = imgElement.naturalWidth || imgElement.width, - sourceHeight = imgElement.naturalHeight || imgElement.height; - - if (this._element === this._originalElement) { - // if the element is the same we need to create a new element - var canvasEl = fabric.util.createCanvasElement(); - canvasEl.width = sourceWidth; - canvasEl.height = sourceHeight; - this._element = canvasEl; - this._filteredEl = canvasEl; - } - else { - // clear the existing element to get new filter data - // also dereference the eventual resized _element - this._element = this._filteredEl; - this._filteredEl.getContext('2d').clearRect(0, 0, sourceWidth, sourceHeight); - // we also need to resize again at next renderAll, so remove saved _lastScaleX/Y - this._lastScaleX = 1; - this._lastScaleY = 1; - } - if (!fabric.filterBackend) { - fabric.filterBackend = fabric.initFilterBackend(); - } - fabric.filterBackend.applyFilters( - filters, this._originalElement, sourceWidth, sourceHeight, this._element, this.cacheKey); - if (this._originalElement.width !== this._element.width || - this._originalElement.height !== this._element.height) { - this._filterScalingX = this._element.width / this._originalElement.width; - this._filterScalingY = this._element.height / this._originalElement.height; - } - return this; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - fabric.util.setImageSmoothing(ctx, this.imageSmoothing); - if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { - this.applyResizeFilters(); - } - this._stroke(ctx); - this._renderPaintInOrder(ctx); - }, - - /** - * Paint the cached copy of the object on the target context. - * it will set the imageSmoothing for the draw operation - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - drawCacheOnCanvas: function (ctx) { - fabric.util.setImageSmoothing(ctx, this.imageSmoothing); - fabric.Object.prototype.drawCacheOnCanvas.call(this, ctx); - }, - - /** - * Decide if the object should cache or not. Create its own cache level - * needsItsOwnCache should be used when the object drawing method requires - * a cache step. None of the fabric classes requires it. - * Generally you do not cache objects in groups because the group outside is cached. - * This is the special image version where we would like to avoid caching where possible. - * Essentially images do not benefit from caching. They may require caching, and in that - * case we do it. Also caching an image usually ends in a loss of details. - * A full performance audit should be done. - * @return {Boolean} - */ - shouldCache: function () { - return this.needsItsOwnCache(); - }, - - _renderFill: function (ctx) { - var elementToDraw = this._element; - if (!elementToDraw) { - return; - } - var scaleX = this._filterScalingX, scaleY = this._filterScalingY, - w = this.width, h = this.height, min = Math.min, max = Math.max, - // crop values cannot be lesser than 0. - cropX = max(this.cropX, 0), cropY = max(this.cropY, 0), - elWidth = elementToDraw.naturalWidth || elementToDraw.width, - elHeight = elementToDraw.naturalHeight || elementToDraw.height, - sX = cropX * scaleX, - sY = cropY * scaleY, - // the width height cannot exceed element width/height, starting from the crop offset. - sW = min(w * scaleX, elWidth - sX), - sH = min(h * scaleY, elHeight - sY), - x = -w / 2, y = -h / 2, - maxDestW = min(w, elWidth / scaleX - cropX), - maxDestH = min(h, elHeight / scaleY - cropY); - - elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH); - }, - - /** - * needed to check if image needs resize - * @private - */ - _needsResize: function () { - var scale = this.getTotalObjectScaling(); - return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); - }, - - /** - * @private - */ - _resetWidthHeight: function () { - this.set(this.getOriginalSize()); - }, - - /** - * The Image class's initialization method. This method is automatically - * called by the constructor. - * @private - * @param {HTMLImageElement|String} element The element representing the image - * @param {Object} [options] Options object - */ - _initElement: function (element, options) { - this.setElement(fabric.util.getById(element), options); - fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); - }, - - /** - * @private - * @param {Object} [options] Options object - */ - _initConfig: function (options) { - options || (options = {}); - this.setOptions(options); - this._setWidthHeight(options); - }, - - /** - * @private - * @param {Array} filters to be initialized - * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created - */ - _initFilters: function (filters, callback) { - if (filters && filters.length) { - fabric.util.enlivenObjects(filters, function (enlivenedObjects) { - callback && callback(enlivenedObjects); - }, 'fabric.Image.filters'); - } - else { - callback && callback(); - } - }, - - /** - * @private - * Set the width and the height of the image object, using the element or the - * options. - * @param {Object} [options] Object with width/height properties - */ - _setWidthHeight: function (options) { - options || (options = {}); - var el = this.getElement(); - this.width = options.width || el.naturalWidth || el.width || 0; - this.height = options.height || el.naturalHeight || el.height || 0; - }, - - /** - * Calculate offset for center and scale factor for the image in order to respect - * the preserveAspectRatio attribute - * @private - * @return {Object} - */ - parsePreserveAspectRatioAttribute: function () { - var pAR = fabric.util.parsePreserveAspectRatioAttribute(this.preserveAspectRatio || ''), - rWidth = this._element.width, rHeight = this._element.height, - scaleX = 1, scaleY = 1, offsetLeft = 0, offsetTop = 0, cropX = 0, cropY = 0, - offset, pWidth = this.width, pHeight = this.height, parsedAttributes = { width: pWidth, height: pHeight }; - if (pAR && (pAR.alignX !== 'none' || pAR.alignY !== 'none')) { - if (pAR.meetOrSlice === 'meet') { - scaleX = scaleY = fabric.util.findScaleToFit(this._element, parsedAttributes); - offset = (pWidth - rWidth * scaleX) / 2; - if (pAR.alignX === 'Min') { - offsetLeft = -offset; - } - if (pAR.alignX === 'Max') { - offsetLeft = offset; - } - offset = (pHeight - rHeight * scaleY) / 2; - if (pAR.alignY === 'Min') { - offsetTop = -offset; - } - if (pAR.alignY === 'Max') { - offsetTop = offset; - } - } - if (pAR.meetOrSlice === 'slice') { - scaleX = scaleY = fabric.util.findScaleToCover(this._element, parsedAttributes); - offset = rWidth - pWidth / scaleX; - if (pAR.alignX === 'Mid') { - cropX = offset / 2; - } - if (pAR.alignX === 'Max') { - cropX = offset; - } - offset = rHeight - pHeight / scaleY; - if (pAR.alignY === 'Mid') { - cropY = offset / 2; - } - if (pAR.alignY === 'Max') { - cropY = offset; - } - rWidth = pWidth / scaleX; - rHeight = pHeight / scaleY; - } - } - else { - scaleX = pWidth / rWidth; - scaleY = pHeight / rHeight; - } - return { - width: rWidth, - height: rHeight, - scaleX: scaleX, - scaleY: scaleY, - offsetLeft: offsetLeft, - offsetTop: offsetTop, - cropX: cropX, - cropY: cropY - }; - } - }); - - /** - * Default CSS class name for canvas - * @static - * @type String - * @default - */ - fabric.Image.CSS_CANVAS = 'canvas-img'; - - /** - * Alias for getSrc - * @static - */ - fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; - - /** - * Creates an instance of fabric.Image from its object representation - * @static - * @param {Object} object Object to create an instance from - * @param {Function} callback Callback to invoke when an image instance is created - */ - fabric.Image.fromObject = function (_object, callback) { - var object = fabric.util.object.clone(_object); - fabric.util.loadImage(object.src, function (img, isError) { - if (isError) { - callback && callback(null, true); - return; - } - fabric.Image.prototype._initFilters.call(object, object.filters, function (filters) { - object.filters = filters || []; - fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function (resizeFilters) { - object.resizeFilter = resizeFilters[0]; - fabric.util.enlivenObjectEnlivables(object, object, function () { - var image = new fabric.Image(img, object); - callback(image, false); - }); - }); - }); - }, null, object.crossOrigin); - }; - - /** - * Creates an instance of fabric.Image from an URL string - * @static - * @param {String} url URL to create an image from - * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not. - * @param {Object} [imgOptions] Options object - */ - fabric.Image.fromURL = function (url, callback, imgOptions) { - fabric.util.loadImage(url, function (img, isError) { - callback && callback(new fabric.Image(img, imgOptions), isError); - }, null, imgOptions && imgOptions.crossOrigin); - }; - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) - * @static - * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement} - */ - fabric.Image.ATTRIBUTE_NAMES = - fabric.SHARED_ATTRIBUTES.concat( - 'x y width height preserveAspectRatio xlink:href crossOrigin image-rendering'.split(' ') - ); - - /** - * Returns {@link fabric.Image} instance from an SVG element - * @static - * @param {SVGElement} element Element to parse - * @param {Object} [options] Options object - * @param {Function} callback Callback to execute when fabric.Image object is created - * @return {fabric.Image} Instance of fabric.Image - */ - fabric.Image.fromElement = function (element, callback, options) { - var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); - fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, - extend((options ? fabric.util.object.clone(options) : {}), parsedAttributes)); - }; - /* _FROM_SVG_END_ */ - -})(typeof exports !== 'undefined' ? exports : this); - - -fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - - /** - * @private - * @return {Number} angle value - */ - _getAngleValueForStraighten: function () { - var angle = this.angle % 360; - if (angle > 0) { - return Math.round((angle - 1) / 90) * 90; - } - return Math.round(angle / 90) * 90; - }, - - /** - * Straightens an object (rotating it from current angle to one of 0, 90, 180, 270, etc. depending on which is closer) - * @return {fabric.Object} thisArg - * @chainable - */ - straighten: function () { - return this.rotate(this._getAngleValueForStraighten()); - }, - - /** - * Same as {@link fabric.Object.prototype.straighten} but with animation - * @param {Object} callbacks Object with callback functions - * @param {Function} [callbacks.onComplete] Invoked on completion - * @param {Function} [callbacks.onChange] Invoked on every step of animation - * @return {fabric.Object} thisArg - */ - fxStraighten: function (callbacks) { - callbacks = callbacks || {}; - - var empty = function () { }, - onComplete = callbacks.onComplete || empty, - onChange = callbacks.onChange || empty, - _this = this; - - return fabric.util.animate({ - target: this, - startValue: this.get('angle'), - endValue: this._getAngleValueForStraighten(), - duration: this.FX_DURATION, - onChange: function (value) { - _this.rotate(value); - onChange(); - }, - onComplete: function () { - _this.setCoords(); - onComplete(); - }, - }); - } -}); - -fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { - - /** - * Straightens object, then rerenders canvas - * @param {fabric.Object} object Object to straighten - * @return {fabric.Canvas} thisArg - * @chainable - */ - straightenObject: function (object) { - object.straighten(); - this.requestRenderAll(); - return this; - }, - - /** - * Same as {@link fabric.Canvas.prototype.straightenObject}, but animated - * @param {fabric.Object} object Object to straighten - * @return {fabric.Canvas} thisArg - */ - fxStraightenObject: function (object) { - return object.fxStraighten({ - onChange: this.requestRenderAllBound - }); - } -}); - - -(function () { - - 'use strict'; - - /** - * Tests if webgl supports certain precision - * @param {WebGL} Canvas WebGL context to test on - * @param {String} Precision to test can be any of following: 'lowp', 'mediump', 'highp' - * @returns {Boolean} Whether the user's browser WebGL supports given precision. - */ - function testPrecision(gl, precision) { - var fragmentSource = 'precision ' + precision + ' float;\nvoid main(){}'; - var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fragmentShader, fragmentSource); - gl.compileShader(fragmentShader); - if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { - return false; - } - return true; - } - - /** - * Indicate whether this filtering backend is supported by the user's browser. - * @param {Number} tileSize check if the tileSize is supported - * @returns {Boolean} Whether the user's browser supports WebGL. - */ - fabric.isWebglSupported = function (tileSize) { - if (fabric.isLikelyNode) { - return false; - } - tileSize = tileSize || fabric.WebglFilterBackend.prototype.tileSize; - var canvas = document.createElement('canvas'); - var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); - var isSupported = false; - // eslint-disable-next-line - if (gl) { - fabric.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); - isSupported = fabric.maxTextureSize >= tileSize; - var precisions = ['highp', 'mediump', 'lowp']; - for (var i = 0; i < 3; i++) { - if (testPrecision(gl, precisions[i])) { - fabric.webGlPrecision = precisions[i]; - break; - }; - } - } - this.isSupported = isSupported; - return isSupported; - }; - - fabric.WebglFilterBackend = WebglFilterBackend; - - /** - * WebGL filter backend. - */ - function WebglFilterBackend(options) { - if (options && options.tileSize) { - this.tileSize = options.tileSize; - } - this.setupGLContext(this.tileSize, this.tileSize); - this.captureGPUInfo(); - }; - - WebglFilterBackend.prototype = /** @lends fabric.WebglFilterBackend.prototype */ { - - tileSize: 2048, - - /** - * Experimental. This object is a sort of repository of help layers used to avoid - * of recreating them during frequent filtering. If you are previewing a filter with - * a slider you probably do not want to create help layers every filter step. - * in this object there will be appended some canvases, created once, resized sometimes - * cleared never. Clearing is left to the developer. - **/ - resources: { - - }, - - /** - * Setup a WebGL context suitable for filtering, and bind any needed event handlers. - */ - setupGLContext: function (width, height) { - this.dispose(); - this.createWebGLCanvas(width, height); - // eslint-disable-next-line - this.aPosition = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); - this.chooseFastestCopyGLTo2DMethod(width, height); - }, - - /** - * Pick a method to copy data from GL context to 2d canvas. In some browsers using - * putImageData is faster than drawImage for that specific operation. - */ - chooseFastestCopyGLTo2DMethod: function (width, height) { - var canMeasurePerf = typeof window.performance !== 'undefined', canUseImageData; - try { - new ImageData(1, 1); - canUseImageData = true; - } - catch (e) { - canUseImageData = false; - } - // eslint-disable-next-line no-undef - var canUseArrayBuffer = typeof ArrayBuffer !== 'undefined'; - // eslint-disable-next-line no-undef - var canUseUint8Clamped = typeof Uint8ClampedArray !== 'undefined'; - - if (!(canMeasurePerf && canUseImageData && canUseArrayBuffer && canUseUint8Clamped)) { - return; - } - - var targetCanvas = fabric.util.createCanvasElement(); - // eslint-disable-next-line no-undef - var imageBuffer = new ArrayBuffer(width * height * 4); - if (fabric.forceGLPutImageData) { - this.imageBuffer = imageBuffer; - this.copyGLTo2D = copyGLTo2DPutImageData; - return; - } - var testContext = { - imageBuffer: imageBuffer, - destinationWidth: width, - destinationHeight: height, - targetCanvas: targetCanvas - }; - var startTime, drawImageTime, putImageDataTime; - targetCanvas.width = width; - targetCanvas.height = height; - - startTime = window.performance.now(); - copyGLTo2DDrawImage.call(testContext, this.gl, testContext); - drawImageTime = window.performance.now() - startTime; - - startTime = window.performance.now(); - copyGLTo2DPutImageData.call(testContext, this.gl, testContext); - putImageDataTime = window.performance.now() - startTime; - - if (drawImageTime > putImageDataTime) { - this.imageBuffer = imageBuffer; - this.copyGLTo2D = copyGLTo2DPutImageData; - } - else { - this.copyGLTo2D = copyGLTo2DDrawImage; - } - }, - - /** - * Create a canvas element and associated WebGL context and attaches them as - * class properties to the GLFilterBackend class. - */ - createWebGLCanvas: function (width, height) { - var canvas = fabric.util.createCanvasElement(); - canvas.width = width; - canvas.height = height; - var glOptions = { - alpha: true, - premultipliedAlpha: false, - depth: false, - stencil: false, - antialias: false - }, - gl = canvas.getContext('webgl', glOptions); - if (!gl) { - gl = canvas.getContext('experimental-webgl', glOptions); - } - if (!gl) { - return; - } - gl.clearColor(0, 0, 0, 0); - // this canvas can fire webglcontextlost and webglcontextrestored - this.canvas = canvas; - this.gl = gl; - }, - - /** - * Attempts to apply the requested filters to the source provided, drawing the filtered output - * to the provided target canvas. - * - * @param {Array} filters The filters to apply. - * @param {HTMLImageElement|HTMLCanvasElement} source The source to be filtered. - * @param {Number} width The width of the source input. - * @param {Number} height The height of the source input. - * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. - * @param {String|undefined} cacheKey A key used to cache resources related to the source. If - * omitted, caching will be skipped. - */ - applyFilters: function (filters, source, width, height, targetCanvas, cacheKey) { - var gl = this.gl; - var cachedTexture; - if (cacheKey) { - cachedTexture = this.getCachedTexture(cacheKey, source); - } - var pipelineState = { - originalWidth: source.width || source.originalWidth, - originalHeight: source.height || source.originalHeight, - sourceWidth: width, - sourceHeight: height, - destinationWidth: width, - destinationHeight: height, - context: gl, - sourceTexture: this.createTexture(gl, width, height, !cachedTexture && source), - targetTexture: this.createTexture(gl, width, height), - originalTexture: cachedTexture || - this.createTexture(gl, width, height, !cachedTexture && source), - passes: filters.length, - webgl: true, - aPosition: this.aPosition, - programCache: this.programCache, - pass: 0, - filterBackend: this, - targetCanvas: targetCanvas - }; - var tempFbo = gl.createFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo); - filters.forEach(function (filter) { filter && filter.applyTo(pipelineState); }); - resizeCanvasIfNeeded(pipelineState); - this.copyGLTo2D(gl, pipelineState); - gl.bindTexture(gl.TEXTURE_2D, null); - gl.deleteTexture(pipelineState.sourceTexture); - gl.deleteTexture(pipelineState.targetTexture); - gl.deleteFramebuffer(tempFbo); - targetCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); - return pipelineState; - }, - - /** - * Detach event listeners, remove references, and clean up caches. - */ - dispose: function () { - if (this.canvas) { - this.canvas = null; - this.gl = null; - } - this.clearWebGLCaches(); - }, - - /** - * Wipe out WebGL-related caches. - */ - clearWebGLCaches: function () { - this.programCache = {}; - this.textureCache = {}; - }, - - /** - * Create a WebGL texture object. - * - * Accepts specific dimensions to initialize the texture to or a source image. - * - * @param {WebGLRenderingContext} gl The GL context to use for creating the texture. - * @param {Number} width The width to initialize the texture at. - * @param {Number} height The height to initialize the texture. - * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source for the texture data. - * @param {Number} filterType gl.NEAREST or gl.LINEAR usually, webgl numeri constants - * @returns {WebGLTexture} - */ - createTexture: function (gl, width, height, textureImageSource, filterType) { - var texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterType || gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterType || gl.NEAREST); - 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); - if (textureImageSource) { - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImageSource); - } - else { - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); - } - return texture; - }, - - /** - * Can be optionally used to get a texture from the cache array - * - * If an existing texture is not found, a new texture is created and cached. - * - * @param {String} uniqueId A cache key to use to find an existing texture. - * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source to use to create the - * texture cache entry if one does not already exist. - */ - getCachedTexture: function (uniqueId, textureImageSource) { - if (this.textureCache[uniqueId]) { - return this.textureCache[uniqueId]; - } - else { - var texture = this.createTexture( - this.gl, textureImageSource.width, textureImageSource.height, textureImageSource); - this.textureCache[uniqueId] = texture; - return texture; - } - }, - - /** - * Clear out cached resources related to a source image that has been - * filtered previously. - * - * @param {String} cacheKey The cache key provided when the source image was filtered. - */ - evictCachesForKey: function (cacheKey) { - if (this.textureCache[cacheKey]) { - this.gl.deleteTexture(this.textureCache[cacheKey]); - delete this.textureCache[cacheKey]; - } - }, - - copyGLTo2D: copyGLTo2DDrawImage, - - /** - * Attempt to extract GPU information strings from a WebGL context. - * - * Useful information when debugging or blacklisting specific GPUs. - * - * @returns {Object} A GPU info object with renderer and vendor strings. - */ - captureGPUInfo: function () { - if (this.gpuInfo) { - return this.gpuInfo; - } - var gl = this.gl, gpuInfo = { renderer: '', vendor: '' }; - if (!gl) { - return gpuInfo; - } - var ext = gl.getExtension('WEBGL_debug_renderer_info'); - if (ext) { - var renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); - var vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL); - if (renderer) { - gpuInfo.renderer = renderer.toLowerCase(); - } - if (vendor) { - gpuInfo.vendor = vendor.toLowerCase(); - } - } - this.gpuInfo = gpuInfo; - return gpuInfo; - }, - }; -})(); - -function resizeCanvasIfNeeded(pipelineState) { - var targetCanvas = pipelineState.targetCanvas, - width = targetCanvas.width, height = targetCanvas.height, - dWidth = pipelineState.destinationWidth, - dHeight = pipelineState.destinationHeight; - - if (width !== dWidth || height !== dHeight) { - targetCanvas.width = dWidth; - targetCanvas.height = dHeight; - } -} - -/** - * Copy an input WebGL canvas on to an output 2D canvas. - * - * The WebGL canvas is assumed to be upside down, with the top-left pixel of the - * desired output image appearing in the bottom-left corner of the WebGL canvas. - * - * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. - * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. - * @param {Object} pipelineState The 2D target canvas to copy on to. - */ -function copyGLTo2DDrawImage(gl, pipelineState) { - var glCanvas = gl.canvas, targetCanvas = pipelineState.targetCanvas, - ctx = targetCanvas.getContext('2d'); - ctx.translate(0, targetCanvas.height); // move it down again - ctx.scale(1, -1); // vertical flip - // where is my image on the big glcanvas? - var sourceY = glCanvas.height - targetCanvas.height; - ctx.drawImage(glCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, - targetCanvas.width, targetCanvas.height); -} - -/** - * Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData - * API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra). - * - * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. - * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. - * @param {Object} pipelineState The 2D target canvas to copy on to. - */ -function copyGLTo2DPutImageData(gl, pipelineState) { - var targetCanvas = pipelineState.targetCanvas, ctx = targetCanvas.getContext('2d'), - dWidth = pipelineState.destinationWidth, - dHeight = pipelineState.destinationHeight, - numBytes = dWidth * dHeight * 4; - - // eslint-disable-next-line no-undef - var u8 = new Uint8Array(this.imageBuffer, 0, numBytes); - // eslint-disable-next-line no-undef - var u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes); - - gl.readPixels(0, 0, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8); - var imgData = new ImageData(u8Clamped, dWidth, dHeight); - ctx.putImageData(imgData, 0, 0); -} - - -(function () { - - 'use strict'; - - var noop = function () { }; - - fabric.Canvas2dFilterBackend = Canvas2dFilterBackend; - - /** - * Canvas 2D filter backend. - */ - function Canvas2dFilterBackend() { }; - - Canvas2dFilterBackend.prototype = /** @lends fabric.Canvas2dFilterBackend.prototype */ { - evictCachesForKey: noop, - dispose: noop, - clearWebGLCaches: noop, - - /** - * Experimental. This object is a sort of repository of help layers used to avoid - * of recreating them during frequent filtering. If you are previewing a filter with - * a slider you probably do not want to create help layers every filter step. - * in this object there will be appended some canvases, created once, resized sometimes - * cleared never. Clearing is left to the developer. - **/ - resources: { - - }, - - /** - * Apply a set of filters against a source image and draw the filtered output - * to the provided destination canvas. - * - * @param {EnhancedFilter} filters The filter to apply. - * @param {HTMLImageElement|HTMLCanvasElement} sourceElement The source to be filtered. - * @param {Number} sourceWidth The width of the source input. - * @param {Number} sourceHeight The height of the source input. - * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. - */ - applyFilters: function (filters, sourceElement, sourceWidth, sourceHeight, targetCanvas) { - var ctx = targetCanvas.getContext('2d'); - ctx.drawImage(sourceElement, 0, 0, sourceWidth, sourceHeight); - var imageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); - var originalImageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); - var pipelineState = { - sourceWidth: sourceWidth, - sourceHeight: sourceHeight, - imageData: imageData, - originalEl: sourceElement, - originalImageData: originalImageData, - canvasEl: targetCanvas, - ctx: ctx, - filterBackend: this, - }; - filters.forEach(function (filter) { filter.applyTo(pipelineState); }); - if (pipelineState.imageData.width !== sourceWidth || pipelineState.imageData.height !== sourceHeight) { - targetCanvas.width = pipelineState.imageData.width; - targetCanvas.height = pipelineState.imageData.height; - } - ctx.putImageData(pipelineState.imageData, 0, 0); - return pipelineState; - }, - - }; -})(); - - -/** - * @namespace fabric.Image.filters - * @memberOf fabric.Image - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#image_filters} - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - */ -fabric.Image = fabric.Image || {}; -fabric.Image.filters = fabric.Image.filters || {}; - -/** - * Root filter class from which all filter classes inherit from - * @class fabric.Image.filters.BaseFilter - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Image.filters.BaseFilter.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'BaseFilter', - - /** - * Array of attributes to send with buffers. do not modify - * @private - */ - - vertexSource: 'attribute vec2 aPosition;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vTexCoord = aPosition;\n' + - 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + - '}', - - fragmentSource: 'precision highp float;\n' + - 'varying vec2 vTexCoord;\n' + - 'uniform sampler2D uTexture;\n' + - 'void main() {\n' + - 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + - '}', - - /** - * Constructor - * @param {Object} [options] Options object - */ - initialize: function (options) { - if (options) { - this.setOptions(options); - } - }, - - /** - * Sets filter's properties from options - * @param {Object} [options] Options object - */ - setOptions: function (options) { - for (var prop in options) { - this[prop] = options[prop]; - } - }, - - /** - * Compile this filter's shader program. - * - * @param {WebGLRenderingContext} gl The GL canvas context to use for shader compilation. - * @param {String} fragmentSource fragmentShader source for compilation - * @param {String} vertexSource vertexShader source for compilation - */ - createProgram: function (gl, fragmentSource, vertexSource) { - fragmentSource = fragmentSource || this.fragmentSource; - vertexSource = vertexSource || this.vertexSource; - if (fabric.webGlPrecision !== 'highp') { - fragmentSource = fragmentSource.replace( - /precision highp float/g, - 'precision ' + fabric.webGlPrecision + ' float' - ); - } - var vertexShader = gl.createShader(gl.VERTEX_SHADER); - gl.shaderSource(vertexShader, vertexSource); - gl.compileShader(vertexShader); - if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { - throw new Error( - // eslint-disable-next-line prefer-template - 'Vertex shader compile error for ' + this.type + ': ' + - gl.getShaderInfoLog(vertexShader) - ); - } - - var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fragmentShader, fragmentSource); - gl.compileShader(fragmentShader); - if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { - throw new Error( - // eslint-disable-next-line prefer-template - 'Fragment shader compile error for ' + this.type + ': ' + - gl.getShaderInfoLog(fragmentShader) - ); - } - - var program = gl.createProgram(); - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - throw new Error( - // eslint-disable-next-line prefer-template - 'Shader link error for "${this.type}" ' + - gl.getProgramInfoLog(program) - ); - } - - var attributeLocations = this.getAttributeLocations(gl, program); - var uniformLocations = this.getUniformLocations(gl, program) || {}; - uniformLocations.uStepW = gl.getUniformLocation(program, 'uStepW'); - uniformLocations.uStepH = gl.getUniformLocation(program, 'uStepH'); - return { - program: program, - attributeLocations: attributeLocations, - uniformLocations: uniformLocations - }; - }, - - /** - * Return a map of attribute names to WebGLAttributeLocation objects. - * - * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. - * @param {WebGLShaderProgram} program The shader program from which to take attribute locations. - * @returns {Object} A map of attribute names to attribute locations. - */ - getAttributeLocations: function (gl, program) { - return { - aPosition: gl.getAttribLocation(program, 'aPosition'), - }; - }, - - /** - * Return a map of uniform names to WebGLUniformLocation objects. - * - * Intended to be overridden by subclasses. - * - * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. - * @param {WebGLShaderProgram} program The shader program from which to take uniform locations. - * @returns {Object} A map of uniform names to uniform locations. - */ - getUniformLocations: function (/* gl, program */) { - // in case i do not need any special uniform i need to return an empty object - return {}; - }, - - /** - * Send attribute data from this filter to its shader program on the GPU. - * - * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. - * @param {Object} attributeLocations A map of shader attribute names to their locations. - */ - sendAttributeData: function (gl, attributeLocations, aPositionData) { - var attributeLocation = attributeLocations.aPosition; - var buffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - gl.enableVertexAttribArray(attributeLocation); - gl.vertexAttribPointer(attributeLocation, 2, gl.FLOAT, false, 0, 0); - gl.bufferData(gl.ARRAY_BUFFER, aPositionData, gl.STATIC_DRAW); - }, - - _setupFrameBuffer: function (options) { - var gl = options.context, width, height; - if (options.passes > 1) { - width = options.destinationWidth; - height = options.destinationHeight; - if (options.sourceWidth !== width || options.sourceHeight !== height) { - gl.deleteTexture(options.targetTexture); - options.targetTexture = options.filterBackend.createTexture(gl, width, height); - } - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, - options.targetTexture, 0); - } - else { - // draw last filter on canvas and not to framebuffer. - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - gl.finish(); - } - }, - - _swapTextures: function (options) { - options.passes--; - options.pass++; - var temp = options.targetTexture; - options.targetTexture = options.sourceTexture; - options.sourceTexture = temp; - }, - - /** - * Generic isNeutral implementation for one parameter based filters. - * Used only in image applyFilters to discard filters that will not have an effect - * on the image - * Other filters may need their own version ( ColorMatrix, HueRotation, gamma, ComposedFilter ) - * @param {Object} options - **/ - isNeutralState: function (/* options */) { - var main = this.mainParameter, - _class = fabric.Image.filters[this.type].prototype; - if (main) { - if (Array.isArray(_class[main])) { - for (var i = _class[main].length; i--;) { - if (this[main][i] !== _class[main][i]) { - return false; - } - } - return true; - } - else { - return _class[main] === this[main]; - } - } - else { - return false; - } - }, - - /** - * Apply this filter to the input image data provided. - * - * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. - * - * @param {Object} options - * @param {Number} options.passes The number of filters remaining to be executed - * @param {Boolean} options.webgl Whether to use webgl to render the filter. - * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. - * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - applyTo: function (options) { - if (options.webgl) { - this._setupFrameBuffer(options); - this.applyToWebGL(options); - this._swapTextures(options); - } - else { - this.applyTo2d(options); - } - }, - - /** - * Retrieves the cached shader. - * @param {Object} options - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - retrieveShader: function (options) { - if (!options.programCache.hasOwnProperty(this.type)) { - options.programCache[this.type] = this.createProgram(options.context); - } - return options.programCache[this.type]; - }, - - /** - * Apply this filter using webgl. - * - * @param {Object} options - * @param {Number} options.passes The number of filters remaining to be executed - * @param {Boolean} options.webgl Whether to use webgl to render the filter. - * @param {WebGLTexture} options.originalTexture The texture of the original input image. - * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. - * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - applyToWebGL: function (options) { - var gl = options.context; - var shader = this.retrieveShader(options); - if (options.pass === 0 && options.originalTexture) { - gl.bindTexture(gl.TEXTURE_2D, options.originalTexture); - } - else { - gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture); - } - gl.useProgram(shader.program); - this.sendAttributeData(gl, shader.attributeLocations, options.aPosition); - - gl.uniform1f(shader.uniformLocations.uStepW, 1 / options.sourceWidth); - gl.uniform1f(shader.uniformLocations.uStepH, 1 / options.sourceHeight); - - this.sendUniformData(gl, shader.uniformLocations); - gl.viewport(0, 0, options.destinationWidth, options.destinationHeight); - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); - }, - - bindAdditionalTexture: function (gl, texture, textureUnit) { - gl.activeTexture(textureUnit); - gl.bindTexture(gl.TEXTURE_2D, texture); - // reset active texture to 0 as usual - gl.activeTexture(gl.TEXTURE0); - }, - - unbindAdditionalTexture: function (gl, textureUnit) { - gl.activeTexture(textureUnit); - gl.bindTexture(gl.TEXTURE_2D, null); - gl.activeTexture(gl.TEXTURE0); - }, - - getMainParameter: function () { - return this[this.mainParameter]; - }, - - setMainParameter: function (value) { - this[this.mainParameter] = value; - }, - - /** - * Send uniform data from this filter to its shader program on the GPU. - * - * Intended to be overridden by subclasses. - * - * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. - * @param {Object} uniformLocations A map of shader uniform names to their locations. - */ - sendUniformData: function (/* gl, uniformLocations */) { - // Intentionally left blank. Override me in subclasses. - }, - - /** - * If needed by a 2d filter, this functions can create an helper canvas to be used - * remember that options.targetCanvas is available for use till end of chain. - */ - createHelpLayer: function (options) { - if (!options.helpLayer) { - var helpLayer = document.createElement('canvas'); - helpLayer.width = options.sourceWidth; - helpLayer.height = options.sourceHeight; - options.helpLayer = helpLayer; - } - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function () { - var object = { type: this.type }, mainP = this.mainParameter; - if (mainP) { - object[mainP] = this[mainP]; - } - return object; - }, - - /** - * Returns a JSON representation of an instance - * @return {Object} JSON - */ - toJSON: function () { - // delegate, not alias - return this.toObject(); - } -}); - -fabric.Image.filters.BaseFilter.fromObject = function (object, callback) { - var filter = new fabric.Image.filters[object.type](object); - callback && callback(filter); - return filter; -}; - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Color Matrix filter class - * @class fabric.Image.filters.ColorMatrix - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.ColorMatrix#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @see {@Link http://www.webwasp.co.uk/tutorials/219/Color_Matrix_Filter.php} - * @see {@Link http://phoboslab.org/log/2013/11/fast-image-filters-with-webgl} - * @example Kodachrome filter - * var filter = new fabric.Image.filters.ColorMatrix({ - * matrix: [ - 1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502, - -0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203, - -0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946, - 0, 0, 0, 1, 0 - ] - * }); - * object.filters.push(filter); - * object.applyFilters(); - */ - filters.ColorMatrix = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.ColorMatrix.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'ColorMatrix', - - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'varying vec2 vTexCoord;\n' + - 'uniform mat4 uColorMatrix;\n' + - 'uniform vec4 uConstants;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'color *= uColorMatrix;\n' + - 'color += uConstants;\n' + - 'gl_FragColor = color;\n' + - '}', - - /** - * Colormatrix for pixels. - * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning - * outside the -1, 1 range. - * 0.0039215686 is the part of 1 that get translated to 1 in 2d - * @param {Array} matrix array of 20 numbers. - * @default - */ - matrix: [ - 1, 0, 0, 0, 0, - 0, 1, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 1, 0 - ], - - mainParameter: 'matrix', - - /** - * Lock the colormatrix on the color part, skipping alpha, mainly for non webgl scenario - * to save some calculation - * @type Boolean - * @default true - */ - colorsOnly: true, - - /** - * Constructor - * @param {Object} [options] Options object - */ - initialize: function (options) { - this.callSuper('initialize', options); - // create a new array instead mutating the prototype with push - this.matrix = this.matrix.slice(0); - }, - - /** - * Apply the ColorMatrix operation to a Uint8Array representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8Array to be filtered. - */ - applyTo2d: function (options) { - var imageData = options.imageData, - data = imageData.data, - iLen = data.length, - m = this.matrix, - r, g, b, a, i, colorsOnly = this.colorsOnly; - - for (i = 0; i < iLen; i += 4) { - r = data[i]; - g = data[i + 1]; - b = data[i + 2]; - if (colorsOnly) { - data[i] = r * m[0] + g * m[1] + b * m[2] + m[4] * 255; - data[i + 1] = r * m[5] + g * m[6] + b * m[7] + m[9] * 255; - data[i + 2] = r * m[10] + g * m[11] + b * m[12] + m[14] * 255; - } - else { - a = data[i + 3]; - data[i] = r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255; - data[i + 1] = r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255; - data[i + 2] = r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255; - data[i + 3] = r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255; - } - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uColorMatrix: gl.getUniformLocation(program, 'uColorMatrix'), - uConstants: gl.getUniformLocation(program, 'uConstants'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - var m = this.matrix, - matrix = [ - m[0], m[1], m[2], m[3], - m[5], m[6], m[7], m[8], - m[10], m[11], m[12], m[13], - m[15], m[16], m[17], m[18] - ], - constants = [m[4], m[9], m[14], m[19]]; - gl.uniformMatrix4fv(uniformLocations.uColorMatrix, false, matrix); - gl.uniform4fv(uniformLocations.uConstants, constants); - }, - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} [callback] function to invoke after filter creation - * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix - */ - fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Brightness filter class - * @class fabric.Image.filters.Brightness - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Brightness({ - * brightness: 0.05 - * }); - * object.filters.push(filter); - * object.applyFilters(); - */ - filters.Brightness = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Brightness', - - /** - * Fragment source for the brightness program - */ - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uBrightness;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'color.rgb += uBrightness;\n' + - 'gl_FragColor = color;\n' + - '}', - - /** - * Brightness value, from -1 to 1. - * translated to -255 to 255 for 2d - * 0.0039215686 is the part of 1 that get translated to 1 in 2d - * @param {Number} brightness - * @default - */ - brightness: 0, - - /** - * Describe the property that is the filter parameter - * @param {String} m - * @default - */ - mainParameter: 'brightness', - - /** - * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. - */ - applyTo2d: function (options) { - if (this.brightness === 0) { - return; - } - var imageData = options.imageData, - data = imageData.data, i, len = data.length, - brightness = Math.round(this.brightness * 255); - for (i = 0; i < len; i += 4) { - data[i] = data[i] + brightness; - data[i + 1] = data[i + 1] + brightness; - data[i + 2] = data[i + 2] + brightness; - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uBrightness: gl.getUniformLocation(program, 'uBrightness'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform1f(uniformLocations.uBrightness, this.brightness); - }, - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness - */ - fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - extend = fabric.util.object.extend, - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Adapted from html5rocks article - * @class fabric.Image.filters.Convolute - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example Sharpen filter - * var filter = new fabric.Image.filters.Convolute({ - * matrix: [ 0, -1, 0, - * -1, 5, -1, - * 0, -1, 0 ] - * }); - * object.filters.push(filter); - * object.applyFilters(); - * canvas.renderAll(); - * @example Blur filter - * var filter = new fabric.Image.filters.Convolute({ - * matrix: [ 1/9, 1/9, 1/9, - * 1/9, 1/9, 1/9, - * 1/9, 1/9, 1/9 ] - * }); - * object.filters.push(filter); - * object.applyFilters(); - * canvas.renderAll(); - * @example Emboss filter - * var filter = new fabric.Image.filters.Convolute({ - * matrix: [ 1, 1, 1, - * 1, 0.7, -1, - * -1, -1, -1 ] - * }); - * object.filters.push(filter); - * object.applyFilters(); - * canvas.renderAll(); - * @example Emboss filter with opaqueness - * var filter = new fabric.Image.filters.Convolute({ - * opaque: true, - * matrix: [ 1, 1, 1, - * 1, 0.7, -1, - * -1, -1, -1 ] - * }); - * object.filters.push(filter); - * object.applyFilters(); - * canvas.renderAll(); - */ - filters.Convolute = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Convolute', - - /* - * Opaque value (true/false) - */ - opaque: false, - - /* - * matrix for the filter, max 9x9 - */ - matrix: [0, 0, 0, 0, 1, 0, 0, 0, 0], - - /** - * Fragment source for the brightness program - */ - fragmentSource: { - Convolute_3_1: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uMatrix[9];\n' + - 'uniform float uStepW;\n' + - 'uniform float uStepH;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = vec4(0, 0, 0, 0);\n' + - 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + - 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + - 'vec2 matrixPos = vec2(uStepW * (w - 1), uStepH * (h - 1));\n' + - 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 3.0 + w)];\n' + - '}\n' + - '}\n' + - 'gl_FragColor = color;\n' + - '}', - Convolute_3_0: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uMatrix[9];\n' + - 'uniform float uStepW;\n' + - 'uniform float uStepH;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = vec4(0, 0, 0, 1);\n' + - 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + - 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + - 'vec2 matrixPos = vec2(uStepW * (w - 1.0), uStepH * (h - 1.0));\n' + - 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 3.0 + w)];\n' + - '}\n' + - '}\n' + - 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + - 'gl_FragColor = color;\n' + - 'gl_FragColor.a = alpha;\n' + - '}', - Convolute_5_1: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uMatrix[25];\n' + - 'uniform float uStepW;\n' + - 'uniform float uStepH;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = vec4(0, 0, 0, 0);\n' + - 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + - 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + - 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + - 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 5.0 + w)];\n' + - '}\n' + - '}\n' + - 'gl_FragColor = color;\n' + - '}', - Convolute_5_0: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uMatrix[25];\n' + - 'uniform float uStepW;\n' + - 'uniform float uStepH;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = vec4(0, 0, 0, 1);\n' + - 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + - 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + - 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + - 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 5.0 + w)];\n' + - '}\n' + - '}\n' + - 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + - 'gl_FragColor = color;\n' + - 'gl_FragColor.a = alpha;\n' + - '}', - Convolute_7_1: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uMatrix[49];\n' + - 'uniform float uStepW;\n' + - 'uniform float uStepH;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = vec4(0, 0, 0, 0);\n' + - 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + - 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + - 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + - 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 7.0 + w)];\n' + - '}\n' + - '}\n' + - 'gl_FragColor = color;\n' + - '}', - Convolute_7_0: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uMatrix[49];\n' + - 'uniform float uStepW;\n' + - 'uniform float uStepH;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = vec4(0, 0, 0, 1);\n' + - 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + - 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + - 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + - 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 7.0 + w)];\n' + - '}\n' + - '}\n' + - 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + - 'gl_FragColor = color;\n' + - 'gl_FragColor.a = alpha;\n' + - '}', - Convolute_9_1: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uMatrix[81];\n' + - 'uniform float uStepW;\n' + - 'uniform float uStepH;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = vec4(0, 0, 0, 0);\n' + - 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + - 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + - 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + - 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 9.0 + w)];\n' + - '}\n' + - '}\n' + - 'gl_FragColor = color;\n' + - '}', - Convolute_9_0: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uMatrix[81];\n' + - 'uniform float uStepW;\n' + - 'uniform float uStepH;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = vec4(0, 0, 0, 1);\n' + - 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + - 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + - 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + - 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 9.0 + w)];\n' + - '}\n' + - '}\n' + - 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + - 'gl_FragColor = color;\n' + - 'gl_FragColor.a = alpha;\n' + - '}', - }, - - /** - * Constructor - * @memberOf fabric.Image.filters.Convolute.prototype - * @param {Object} [options] Options object - * @param {Boolean} [options.opaque=false] Opaque value (true/false) - * @param {Array} [options.matrix] Filter matrix - */ - - - /** - * Retrieves the cached shader. - * @param {Object} options - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - retrieveShader: function (options) { - var size = Math.sqrt(this.matrix.length); - var cacheKey = this.type + '_' + size + '_' + (this.opaque ? 1 : 0); - var shaderSource = this.fragmentSource[cacheKey]; - if (!options.programCache.hasOwnProperty(cacheKey)) { - options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); - } - return options.programCache[cacheKey]; - }, - - /** - * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. - */ - applyTo2d: function (options) { - var imageData = options.imageData, - data = imageData.data, - weights = this.matrix, - side = Math.round(Math.sqrt(weights.length)), - halfSide = Math.floor(side / 2), - sw = imageData.width, - sh = imageData.height, - output = options.ctx.createImageData(sw, sh), - dst = output.data, - // go through the destination image pixels - alphaFac = this.opaque ? 1 : 0, - r, g, b, a, dstOff, - scx, scy, srcOff, wt, - x, y, cx, cy; - - for (y = 0; y < sh; y++) { - for (x = 0; x < sw; x++) { - dstOff = (y * sw + x) * 4; - // calculate the weighed sum of the source image pixels that - // fall under the convolution matrix - r = 0; g = 0; b = 0; a = 0; - - for (cy = 0; cy < side; cy++) { - for (cx = 0; cx < side; cx++) { - scy = y + cy - halfSide; - scx = x + cx - halfSide; - - // eslint-disable-next-line max-depth - if (scy < 0 || scy >= sh || scx < 0 || scx >= sw) { - continue; - } - - srcOff = (scy * sw + scx) * 4; - wt = weights[cy * side + cx]; - - r += data[srcOff] * wt; - g += data[srcOff + 1] * wt; - b += data[srcOff + 2] * wt; - // eslint-disable-next-line max-depth - if (!alphaFac) { - a += data[srcOff + 3] * wt; - } - } - } - dst[dstOff] = r; - dst[dstOff + 1] = g; - dst[dstOff + 2] = b; - if (!alphaFac) { - dst[dstOff + 3] = a; - } - else { - dst[dstOff + 3] = data[dstOff + 3]; - } - } - } - options.imageData = output; - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uMatrix: gl.getUniformLocation(program, 'uMatrix'), - uOpaque: gl.getUniformLocation(program, 'uOpaque'), - uHalfSize: gl.getUniformLocation(program, 'uHalfSize'), - uSize: gl.getUniformLocation(program, 'uSize'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform1fv(uniformLocations.uMatrix, this.matrix); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function () { - return extend(this.callSuper('toObject'), { - opaque: this.opaque, - matrix: this.matrix - }); - } - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute - */ - fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Grayscale image filter class - * @class fabric.Image.filters.Grayscale - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Grayscale(); - * object.filters.push(filter); - * object.applyFilters(); - */ - filters.Grayscale = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Grayscale', - - fragmentSource: { - average: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'float average = (color.r + color.b + color.g) / 3.0;\n' + - 'gl_FragColor = vec4(average, average, average, color.a);\n' + - '}', - lightness: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform int uMode;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 col = texture2D(uTexture, vTexCoord);\n' + - 'float average = (max(max(col.r, col.g),col.b) + min(min(col.r, col.g),col.b)) / 2.0;\n' + - 'gl_FragColor = vec4(average, average, average, col.a);\n' + - '}', - luminosity: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform int uMode;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 col = texture2D(uTexture, vTexCoord);\n' + - 'float average = 0.21 * col.r + 0.72 * col.g + 0.07 * col.b;\n' + - 'gl_FragColor = vec4(average, average, average, col.a);\n' + - '}', - }, - - - /** - * Grayscale mode, between 'average', 'lightness', 'luminosity' - * @param {String} type - * @default - */ - mode: 'average', - - mainParameter: 'mode', - - /** - * Apply the Grayscale operation to a Uint8Array representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8Array to be filtered. - */ - applyTo2d: function (options) { - var imageData = options.imageData, - data = imageData.data, i, - len = data.length, value, - mode = this.mode; - for (i = 0; i < len; i += 4) { - if (mode === 'average') { - value = (data[i] + data[i + 1] + data[i + 2]) / 3; - } - else if (mode === 'lightness') { - value = (Math.min(data[i], data[i + 1], data[i + 2]) + - Math.max(data[i], data[i + 1], data[i + 2])) / 2; - } - else if (mode === 'luminosity') { - value = 0.21 * data[i] + 0.72 * data[i + 1] + 0.07 * data[i + 2]; - } - data[i] = value; - data[i + 1] = value; - data[i + 2] = value; - } - }, - - /** - * Retrieves the cached shader. - * @param {Object} options - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - retrieveShader: function (options) { - var cacheKey = this.type + '_' + this.mode; - if (!options.programCache.hasOwnProperty(cacheKey)) { - var shaderSource = this.fragmentSource[this.mode]; - options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); - } - return options.programCache[cacheKey]; - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uMode: gl.getUniformLocation(program, 'uMode'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - // default average mode. - var mode = 1; - gl.uniform1i(uniformLocations.uMode, mode); - }, - - /** - * Grayscale filter isNeutralState implementation - * The filter is never neutral - * on the image - **/ - isNeutralState: function () { - return false; - }, - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale - */ - fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Invert filter class - * @class fabric.Image.filters.Invert - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Invert(); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); - */ - filters.Invert = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Invert', - - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform int uInvert;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'if (uInvert == 1) {\n' + - 'gl_FragColor = vec4(1.0 - color.r,1.0 -color.g,1.0 -color.b,color.a);\n' + - '} else {\n' + - 'gl_FragColor = color;\n' + - '}\n' + - '}', - - /** - * Filter invert. if false, does nothing - * @param {Boolean} invert - * @default - */ - invert: true, - - mainParameter: 'invert', - - /** - * Apply the Invert operation to a Uint8Array representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8Array to be filtered. - */ - applyTo2d: function (options) { - var imageData = options.imageData, - data = imageData.data, i, - len = data.length; - for (i = 0; i < len; i += 4) { - data[i] = 255 - data[i]; - data[i + 1] = 255 - data[i + 1]; - data[i + 2] = 255 - data[i + 2]; - } - }, - - /** - * Invert filter isNeutralState implementation - * Used only in image applyFilters to discard filters that will not have an effect - * on the image - * @param {Object} options - **/ - isNeutralState: function () { - return !this.invert; - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uInvert: gl.getUniformLocation(program, 'uInvert'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform1i(uniformLocations.uInvert, this.invert); - }, - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert - */ - fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject; - - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - extend = fabric.util.object.extend, - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Noise filter class - * @class fabric.Image.filters.Noise - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Noise({ - * noise: 700 - * }); - * object.filters.push(filter); - * object.applyFilters(); - * canvas.renderAll(); - */ - filters.Noise = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Noise', - - /** - * Fragment source for the noise program - */ - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uStepH;\n' + - 'uniform float uNoise;\n' + - 'uniform float uSeed;\n' + - 'varying vec2 vTexCoord;\n' + - 'float rand(vec2 co, float seed, float vScale) {\n' + - 'return fract(sin(dot(co.xy * vScale ,vec2(12.9898 , 78.233))) * 43758.5453 * (seed + 0.01) / 2.0);\n' + - '}\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'color.rgb += (0.5 - rand(vTexCoord, uSeed, 0.1 / uStepH)) * uNoise;\n' + - 'gl_FragColor = color;\n' + - '}', - - /** - * Describe the property that is the filter parameter - * @param {String} m - * @default - */ - mainParameter: 'noise', - - /** - * Noise value, from - * @param {Number} noise - * @default - */ - noise: 0, - - /** - * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. - */ - applyTo2d: function (options) { - if (this.noise === 0) { - return; - } - var imageData = options.imageData, - data = imageData.data, i, len = data.length, - noise = this.noise, rand; - - for (i = 0, len = data.length; i < len; i += 4) { - - rand = (0.5 - Math.random()) * noise; - - data[i] += rand; - data[i + 1] += rand; - data[i + 2] += rand; - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uNoise: gl.getUniformLocation(program, 'uNoise'), - uSeed: gl.getUniformLocation(program, 'uSeed'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform1f(uniformLocations.uNoise, this.noise / 255); - gl.uniform1f(uniformLocations.uSeed, Math.random()); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function () { - return extend(this.callSuper('toObject'), { - noise: this.noise - }); - } - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise - */ - fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Pixelate filter class - * @class fabric.Image.filters.Pixelate - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Pixelate({ - * blocksize: 8 - * }); - * object.filters.push(filter); - * object.applyFilters(); - */ - filters.Pixelate = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Pixelate', - - blocksize: 4, - - mainParameter: 'blocksize', - - /** - * Fragment source for the Pixelate program - */ - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uBlocksize;\n' + - 'uniform float uStepW;\n' + - 'uniform float uStepH;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'float blockW = uBlocksize * uStepW;\n' + - 'float blockH = uBlocksize * uStepW;\n' + - 'int posX = int(vTexCoord.x / blockW);\n' + - 'int posY = int(vTexCoord.y / blockH);\n' + - 'float fposX = float(posX);\n' + - 'float fposY = float(posY);\n' + - 'vec2 squareCoords = vec2(fposX * blockW, fposY * blockH);\n' + - 'vec4 color = texture2D(uTexture, squareCoords);\n' + - 'gl_FragColor = color;\n' + - '}', - - /** - * Apply the Pixelate operation to a Uint8ClampedArray representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. - */ - applyTo2d: function (options) { - var imageData = options.imageData, - data = imageData.data, - iLen = imageData.height, - jLen = imageData.width, - index, i, j, r, g, b, a, - _i, _j, _iLen, _jLen; - - for (i = 0; i < iLen; i += this.blocksize) { - for (j = 0; j < jLen; j += this.blocksize) { - - index = (i * 4) * jLen + (j * 4); - - r = data[index]; - g = data[index + 1]; - b = data[index + 2]; - a = data[index + 3]; - - _iLen = Math.min(i + this.blocksize, iLen); - _jLen = Math.min(j + this.blocksize, jLen); - for (_i = i; _i < _iLen; _i++) { - for (_j = j; _j < _jLen; _j++) { - index = (_i * 4) * jLen + (_j * 4); - data[index] = r; - data[index + 1] = g; - data[index + 2] = b; - data[index + 3] = a; - } - } - } - } - }, - - /** - * Indicate when the filter is not gonna apply changes to the image - **/ - isNeutralState: function () { - return this.blocksize === 1; - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uBlocksize: gl.getUniformLocation(program, 'uBlocksize'), - uStepW: gl.getUniformLocation(program, 'uStepW'), - uStepH: gl.getUniformLocation(program, 'uStepH'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform1f(uniformLocations.uBlocksize, this.blocksize); - }, - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate - */ - fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - extend = fabric.util.object.extend, - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Remove white filter class - * @class fabric.Image.filters.RemoveColor - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.RemoveColor#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.RemoveColor({ - * threshold: 0.2, - * }); - * object.filters.push(filter); - * object.applyFilters(); - * canvas.renderAll(); - */ - filters.RemoveColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.RemoveColor.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'RemoveColor', - - /** - * Color to remove, in any format understood by fabric.Color. - * @param {String} type - * @default - */ - color: '#FFFFFF', - - /** - * Fragment source for the brightness program - */ - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform vec4 uLow;\n' + - 'uniform vec4 uHigh;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + - 'if(all(greaterThan(gl_FragColor.rgb,uLow.rgb)) && all(greaterThan(uHigh.rgb,gl_FragColor.rgb))) {\n' + - 'gl_FragColor.a = 0.0;\n' + - '}\n' + - '}', - - /** - * distance to actual color, as value up or down from each r,g,b - * between 0 and 1 - **/ - distance: 0.02, - - /** - * For color to remove inside distance, use alpha channel for a smoother deletion - * NOT IMPLEMENTED YET - **/ - useAlpha: false, - - /** - * Constructor - * @memberOf fabric.Image.filters.RemoveWhite.prototype - * @param {Object} [options] Options object - * @param {Number} [options.color=#RRGGBB] Threshold value - * @param {Number} [options.distance=10] Distance value - */ - - /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo2d: function (options) { - var imageData = options.imageData, - data = imageData.data, i, - distance = this.distance * 255, - r, g, b, - source = new fabric.Color(this.color).getSource(), - lowC = [ - source[0] - distance, - source[1] - distance, - source[2] - distance, - ], - highC = [ - source[0] + distance, - source[1] + distance, - source[2] + distance, - ]; - - - for (i = 0; i < data.length; i += 4) { - r = data[i]; - g = data[i + 1]; - b = data[i + 2]; - - if (r > lowC[0] && - g > lowC[1] && - b > lowC[2] && - r < highC[0] && - g < highC[1] && - b < highC[2]) { - data[i + 3] = 0; - } - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uLow: gl.getUniformLocation(program, 'uLow'), - uHigh: gl.getUniformLocation(program, 'uHigh'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - var source = new fabric.Color(this.color).getSource(), - distance = parseFloat(this.distance), - lowC = [ - 0 + source[0] / 255 - distance, - 0 + source[1] / 255 - distance, - 0 + source[2] / 255 - distance, - 1 - ], - highC = [ - source[0] / 255 + distance, - source[1] / 255 + distance, - source[2] / 255 + distance, - 1 - ]; - gl.uniform4fv(uniformLocations.uLow, lowC); - gl.uniform4fv(uniformLocations.uHigh, highC); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function () { - return extend(this.callSuper('toObject'), { - color: this.color, - distance: this.distance - }); - } - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite - */ - fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - var matrices = { - Brownie: [ - 0.59970, 0.34553, -0.27082, 0, 0.186, - -0.03770, 0.86095, 0.15059, 0, -0.1449, - 0.24113, -0.07441, 0.44972, 0, -0.02965, - 0, 0, 0, 1, 0 - ], - Vintage: [ - 0.62793, 0.32021, -0.03965, 0, 0.03784, - 0.02578, 0.64411, 0.03259, 0, 0.02926, - 0.04660, -0.08512, 0.52416, 0, 0.02023, - 0, 0, 0, 1, 0 - ], - Kodachrome: [ - 1.12855, -0.39673, -0.03992, 0, 0.24991, - -0.16404, 1.08352, -0.05498, 0, 0.09698, - -0.16786, -0.56034, 1.60148, 0, 0.13972, - 0, 0, 0, 1, 0 - ], - Technicolor: [ - 1.91252, -0.85453, -0.09155, 0, 0.04624, - -0.30878, 1.76589, -0.10601, 0, -0.27589, - -0.23110, -0.75018, 1.84759, 0, 0.12137, - 0, 0, 0, 1, 0 - ], - Polaroid: [ - 1.438, -0.062, -0.062, 0, 0, - -0.122, 1.378, -0.122, 0, 0, - -0.016, -0.016, 1.483, 0, 0, - 0, 0, 0, 1, 0 - ], - Sepia: [ - 0.393, 0.769, 0.189, 0, 0, - 0.349, 0.686, 0.168, 0, 0, - 0.272, 0.534, 0.131, 0, 0, - 0, 0, 0, 1, 0 - ], - BlackWhite: [ - 1.5, 1.5, 1.5, 0, -1, - 1.5, 1.5, 1.5, 0, -1, - 1.5, 1.5, 1.5, 0, -1, - 0, 0, 0, 1, 0, - ] - }; - - for (var key in matrices) { - filters[key] = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.Sepia.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: key, - - /** - * Colormatrix for the effect - * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning - * outside the -1, 1 range. - * @param {Array} matrix array of 20 numbers. - * @default - */ - matrix: matrices[key], - - /** - * Lock the matrix export for this kind of static, parameter less filters. - */ - mainParameter: false, - /** - * Lock the colormatrix on the color part, skipping alpha - */ - colorsOnly: true, - - }); - fabric.Image.filters[key].fromObject = fabric.Image.filters.BaseFilter.fromObject; - } -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - 'use strict'; - - var fabric = global.fabric, - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Color Blend filter class - * @class fabric.Image.filter.BlendColor - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @example - * var filter = new fabric.Image.filters.BlendColor({ - * color: '#000', - * mode: 'multiply' - * }); - * - * var filter = new fabric.Image.filters.BlendImage({ - * image: fabricImageObject, - * mode: 'multiply', - * alpha: 0.5 - * }); - * object.filters.push(filter); - * object.applyFilters(); - * canvas.renderAll(); - */ - - filters.BlendColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blend.prototype */ { - type: 'BlendColor', - - /** - * Color to make the blend operation with. default to a reddish color since black or white - * gives always strong result. - * @type String - * @default - **/ - color: '#F95C63', - - /** - * Blend mode for the filter: one of multiply, add, diff, screen, subtract, - * darken, lighten, overlay, exclusion, tint. - * @type String - * @default - **/ - mode: 'multiply', - - /** - * alpha value. represent the strength of the blend color operation. - * @type Number - * @default - **/ - alpha: 1, - - /** - * Fragment source for the Multiply program - */ - fragmentSource: { - multiply: 'gl_FragColor.rgb *= uColor.rgb;\n', - screen: 'gl_FragColor.rgb = 1.0 - (1.0 - gl_FragColor.rgb) * (1.0 - uColor.rgb);\n', - add: 'gl_FragColor.rgb += uColor.rgb;\n', - diff: 'gl_FragColor.rgb = abs(gl_FragColor.rgb - uColor.rgb);\n', - subtract: 'gl_FragColor.rgb -= uColor.rgb;\n', - lighten: 'gl_FragColor.rgb = max(gl_FragColor.rgb, uColor.rgb);\n', - darken: 'gl_FragColor.rgb = min(gl_FragColor.rgb, uColor.rgb);\n', - exclusion: 'gl_FragColor.rgb += uColor.rgb - 2.0 * (uColor.rgb * gl_FragColor.rgb);\n', - overlay: 'if (uColor.r < 0.5) {\n' + - 'gl_FragColor.r *= 2.0 * uColor.r;\n' + - '} else {\n' + - 'gl_FragColor.r = 1.0 - 2.0 * (1.0 - gl_FragColor.r) * (1.0 - uColor.r);\n' + - '}\n' + - 'if (uColor.g < 0.5) {\n' + - 'gl_FragColor.g *= 2.0 * uColor.g;\n' + - '} else {\n' + - 'gl_FragColor.g = 1.0 - 2.0 * (1.0 - gl_FragColor.g) * (1.0 - uColor.g);\n' + - '}\n' + - 'if (uColor.b < 0.5) {\n' + - 'gl_FragColor.b *= 2.0 * uColor.b;\n' + - '} else {\n' + - 'gl_FragColor.b = 1.0 - 2.0 * (1.0 - gl_FragColor.b) * (1.0 - uColor.b);\n' + - '}\n', - tint: 'gl_FragColor.rgb *= (1.0 - uColor.a);\n' + - 'gl_FragColor.rgb += uColor.rgb;\n', - }, - - /** - * build the fragment source for the filters, joining the common part with - * the specific one. - * @param {String} mode the mode of the filter, a key of this.fragmentSource - * @return {String} the source to be compiled - * @private - */ - buildSource: function (mode) { - return 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform vec4 uColor;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'gl_FragColor = color;\n' + - 'if (color.a > 0.0) {\n' + - this.fragmentSource[mode] + - '}\n' + - '}'; - }, - - /** - * Retrieves the cached shader. - * @param {Object} options - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - retrieveShader: function (options) { - var cacheKey = this.type + '_' + this.mode, shaderSource; - if (!options.programCache.hasOwnProperty(cacheKey)) { - shaderSource = this.buildSource(this.mode); - options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); - } - return options.programCache[cacheKey]; - }, - - /** - * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. - */ - applyTo2d: function (options) { - var imageData = options.imageData, - data = imageData.data, iLen = data.length, - tr, tg, tb, - r, g, b, - source, alpha1 = 1 - this.alpha; - - source = new fabric.Color(this.color).getSource(); - tr = source[0] * this.alpha; - tg = source[1] * this.alpha; - tb = source[2] * this.alpha; - - for (var i = 0; i < iLen; i += 4) { - - r = data[i]; - g = data[i + 1]; - b = data[i + 2]; - - switch (this.mode) { - case 'multiply': - data[i] = r * tr / 255; - data[i + 1] = g * tg / 255; - data[i + 2] = b * tb / 255; - break; - case 'screen': - data[i] = 255 - (255 - r) * (255 - tr) / 255; - data[i + 1] = 255 - (255 - g) * (255 - tg) / 255; - data[i + 2] = 255 - (255 - b) * (255 - tb) / 255; - break; - case 'add': - data[i] = r + tr; - data[i + 1] = g + tg; - data[i + 2] = b + tb; - break; - case 'diff': - case 'difference': - data[i] = Math.abs(r - tr); - data[i + 1] = Math.abs(g - tg); - data[i + 2] = Math.abs(b - tb); - break; - case 'subtract': - data[i] = r - tr; - data[i + 1] = g - tg; - data[i + 2] = b - tb; - break; - case 'darken': - data[i] = Math.min(r, tr); - data[i + 1] = Math.min(g, tg); - data[i + 2] = Math.min(b, tb); - break; - case 'lighten': - data[i] = Math.max(r, tr); - data[i + 1] = Math.max(g, tg); - data[i + 2] = Math.max(b, tb); - break; - case 'overlay': - data[i] = tr < 128 ? (2 * r * tr / 255) : (255 - 2 * (255 - r) * (255 - tr) / 255); - data[i + 1] = tg < 128 ? (2 * g * tg / 255) : (255 - 2 * (255 - g) * (255 - tg) / 255); - data[i + 2] = tb < 128 ? (2 * b * tb / 255) : (255 - 2 * (255 - b) * (255 - tb) / 255); - break; - case 'exclusion': - data[i] = tr + r - ((2 * tr * r) / 255); - data[i + 1] = tg + g - ((2 * tg * g) / 255); - data[i + 2] = tb + b - ((2 * tb * b) / 255); - break; - case 'tint': - data[i] = tr + r * alpha1; - data[i + 1] = tg + g * alpha1; - data[i + 2] = tb + b * alpha1; - } - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uColor: gl.getUniformLocation(program, 'uColor'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - var source = new fabric.Color(this.color).getSource(); - source[0] = this.alpha * source[0] / 255; - source[1] = this.alpha * source[1] / 255; - source[2] = this.alpha * source[2] / 255; - source[3] = this.alpha; - gl.uniform4fv(uniformLocations.uColor, source); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function () { - return { - type: this.type, - color: this.color, - mode: this.mode, - alpha: this.alpha - }; - } - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor - */ - fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - 'use strict'; - - var fabric = global.fabric, - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Image Blend filter class - * @class fabric.Image.filter.BlendImage - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @example - * var filter = new fabric.Image.filters.BlendColor({ - * color: '#000', - * mode: 'multiply' - * }); - * - * var filter = new fabric.Image.filters.BlendImage({ - * image: fabricImageObject, - * mode: 'multiply', - * alpha: 0.5 - * }); - * object.filters.push(filter); - * object.applyFilters(); - * canvas.renderAll(); - */ - - filters.BlendImage = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.BlendImage.prototype */ { - type: 'BlendImage', - - /** - * Color to make the blend operation with. default to a reddish color since black or white - * gives always strong result. - **/ - image: null, - - /** - * Blend mode for the filter (one of "multiply", "mask") - * @type String - * @default - **/ - mode: 'multiply', - - /** - * alpha value. represent the strength of the blend image operation. - * not implemented. - **/ - alpha: 1, - - vertexSource: 'attribute vec2 aPosition;\n' + - 'varying vec2 vTexCoord;\n' + - 'varying vec2 vTexCoord2;\n' + - 'uniform mat3 uTransformMatrix;\n' + - 'void main() {\n' + - 'vTexCoord = aPosition;\n' + - 'vTexCoord2 = (uTransformMatrix * vec3(aPosition, 1.0)).xy;\n' + - 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + - '}', - - /** - * Fragment source for the Multiply program - */ - fragmentSource: { - multiply: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform sampler2D uImage;\n' + - 'uniform vec4 uColor;\n' + - 'varying vec2 vTexCoord;\n' + - 'varying vec2 vTexCoord2;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + - 'color.rgba *= color2.rgba;\n' + - 'gl_FragColor = color;\n' + - '}', - mask: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform sampler2D uImage;\n' + - 'uniform vec4 uColor;\n' + - 'varying vec2 vTexCoord;\n' + - 'varying vec2 vTexCoord2;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + - 'color.a = color2.a;\n' + - 'gl_FragColor = color;\n' + - '}', - }, - - /** - * Retrieves the cached shader. - * @param {Object} options - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - retrieveShader: function (options) { - var cacheKey = this.type + '_' + this.mode; - var shaderSource = this.fragmentSource[this.mode]; - if (!options.programCache.hasOwnProperty(cacheKey)) { - options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); - } - return options.programCache[cacheKey]; - }, - - applyToWebGL: function (options) { - // load texture to blend. - var gl = options.context, - texture = this.createTexture(options.filterBackend, this.image); - this.bindAdditionalTexture(gl, texture, gl.TEXTURE1); - this.callSuper('applyToWebGL', options); - this.unbindAdditionalTexture(gl, gl.TEXTURE1); - }, - - createTexture: function (backend, image) { - return backend.getCachedTexture(image.cacheKey, image._element); - }, - - /** - * Calculate a transformMatrix to adapt the image to blend over - * @param {Object} options - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - calculateMatrix: function () { - var image = this.image, - width = image._element.width, - height = image._element.height; - return [ - 1 / image.scaleX, 0, 0, - 0, 1 / image.scaleY, 0, - -image.left / width, -image.top / height, 1 - ]; - }, - - /** - * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. - */ - applyTo2d: function (options) { - var imageData = options.imageData, - resources = options.filterBackend.resources, - data = imageData.data, iLen = data.length, - width = imageData.width, - height = imageData.height, - tr, tg, tb, ta, - r, g, b, a, - canvas1, context, image = this.image, blendData; - - if (!resources.blendImage) { - resources.blendImage = fabric.util.createCanvasElement(); - } - canvas1 = resources.blendImage; - context = canvas1.getContext('2d'); - if (canvas1.width !== width || canvas1.height !== height) { - canvas1.width = width; - canvas1.height = height; - } - else { - context.clearRect(0, 0, width, height); - } - context.setTransform(image.scaleX, 0, 0, image.scaleY, image.left, image.top); - context.drawImage(image._element, 0, 0, width, height); - blendData = context.getImageData(0, 0, width, height).data; - for (var i = 0; i < iLen; i += 4) { - - r = data[i]; - g = data[i + 1]; - b = data[i + 2]; - a = data[i + 3]; - - tr = blendData[i]; - tg = blendData[i + 1]; - tb = blendData[i + 2]; - ta = blendData[i + 3]; - - switch (this.mode) { - case 'multiply': - data[i] = r * tr / 255; - data[i + 1] = g * tg / 255; - data[i + 2] = b * tb / 255; - data[i + 3] = a * ta / 255; - break; - case 'mask': - data[i + 3] = ta; - break; - } - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uTransformMatrix: gl.getUniformLocation(program, 'uTransformMatrix'), - uImage: gl.getUniformLocation(program, 'uImage'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - var matrix = this.calculateMatrix(); - gl.uniform1i(uniformLocations.uImage, 1); // texture unit 1. - gl.uniformMatrix3fv(uniformLocations.uTransformMatrix, false, matrix); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function () { - return { - type: this.type, - image: this.image && this.image.toObject(), - mode: this.mode, - alpha: this.alpha - }; - } - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} callback to be invoked after filter creation - * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage - */ - fabric.Image.filters.BlendImage.fromObject = function (object, callback) { - fabric.Image.fromObject(object.image, function (image) { - var options = fabric.util.object.clone(object); - options.image = image; - callback(new fabric.Image.filters.BlendImage(options)); - }); - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), pow = Math.pow, floor = Math.floor, - sqrt = Math.sqrt, abs = Math.abs, round = Math.round, sin = Math.sin, - ceil = Math.ceil, - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Resize image filter class - * @class fabric.Image.filters.Resize - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Resize(); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); - */ - filters.Resize = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Resize.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Resize', - - /** - * Resize type - * for webgl resizeType is just lanczos, for canvas2d can be: - * bilinear, hermite, sliceHack, lanczos. - * @param {String} resizeType - * @default - */ - resizeType: 'hermite', - - /** - * Scale factor for resizing, x axis - * @param {Number} scaleX - * @default - */ - scaleX: 1, - - /** - * Scale factor for resizing, y axis - * @param {Number} scaleY - * @default - */ - scaleY: 1, - - /** - * LanczosLobes parameter for lanczos filter, valid for resizeType lanczos - * @param {Number} lanczosLobes - * @default - */ - lanczosLobes: 3, - - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uDelta: gl.getUniformLocation(program, 'uDelta'), - uTaps: gl.getUniformLocation(program, 'uTaps'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform2fv(uniformLocations.uDelta, this.horizontal ? [1 / this.width, 0] : [0, 1 / this.height]); - gl.uniform1fv(uniformLocations.uTaps, this.taps); - }, - - /** - * Retrieves the cached shader. - * @param {Object} options - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - retrieveShader: function (options) { - var filterWindow = this.getFilterWindow(), cacheKey = this.type + '_' + filterWindow; - if (!options.programCache.hasOwnProperty(cacheKey)) { - var fragmentShader = this.generateShader(filterWindow); - options.programCache[cacheKey] = this.createProgram(options.context, fragmentShader); - } - return options.programCache[cacheKey]; - }, - - getFilterWindow: function () { - var scale = this.tempScale; - return Math.ceil(this.lanczosLobes / scale); - }, - - getTaps: function () { - var lobeFunction = this.lanczosCreate(this.lanczosLobes), scale = this.tempScale, - filterWindow = this.getFilterWindow(), taps = new Array(filterWindow); - for (var i = 1; i <= filterWindow; i++) { - taps[i - 1] = lobeFunction(i * scale); - } - return taps; - }, - - /** - * Generate vertex and shader sources from the necessary steps numbers - * @param {Number} filterWindow - */ - generateShader: function (filterWindow) { - var offsets = new Array(filterWindow), - fragmentShader = this.fragmentSourceTOP, filterWindow; - - for (var i = 1; i <= filterWindow; i++) { - offsets[i - 1] = i + '.0 * uDelta'; - } - - fragmentShader += 'uniform float uTaps[' + filterWindow + '];\n'; - fragmentShader += 'void main() {\n'; - fragmentShader += ' vec4 color = texture2D(uTexture, vTexCoord);\n'; - fragmentShader += ' float sum = 1.0;\n'; - - offsets.forEach(function (offset, i) { - fragmentShader += ' color += texture2D(uTexture, vTexCoord + ' + offset + ') * uTaps[' + i + '];\n'; - fragmentShader += ' color += texture2D(uTexture, vTexCoord - ' + offset + ') * uTaps[' + i + '];\n'; - fragmentShader += ' sum += 2.0 * uTaps[' + i + '];\n'; - }); - fragmentShader += ' gl_FragColor = color / sum;\n'; - fragmentShader += '}'; - return fragmentShader; - }, - - fragmentSourceTOP: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform vec2 uDelta;\n' + - 'varying vec2 vTexCoord;\n', - - /** - * Apply the resize filter to the image - * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. - * - * @param {Object} options - * @param {Number} options.passes The number of filters remaining to be executed - * @param {Boolean} options.webgl Whether to use webgl to render the filter. - * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. - * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - applyTo: function (options) { - if (options.webgl) { - options.passes++; - this.width = options.sourceWidth; - this.horizontal = true; - this.dW = Math.round(this.width * this.scaleX); - this.dH = options.sourceHeight; - this.tempScale = this.dW / this.width; - this.taps = this.getTaps(); - options.destinationWidth = this.dW; - this._setupFrameBuffer(options); - this.applyToWebGL(options); - this._swapTextures(options); - options.sourceWidth = options.destinationWidth; - - this.height = options.sourceHeight; - this.horizontal = false; - this.dH = Math.round(this.height * this.scaleY); - this.tempScale = this.dH / this.height; - this.taps = this.getTaps(); - options.destinationHeight = this.dH; - this._setupFrameBuffer(options); - this.applyToWebGL(options); - this._swapTextures(options); - options.sourceHeight = options.destinationHeight; - } - else { - this.applyTo2d(options); - } - }, - - isNeutralState: function () { - return this.scaleX === 1 && this.scaleY === 1; - }, - - lanczosCreate: function (lobes) { - return function (x) { - if (x >= lobes || x <= -lobes) { - return 0.0; - } - if (x < 1.19209290E-07 && x > -1.19209290E-07) { - return 1.0; - } - x *= Math.PI; - var xx = x / lobes; - return (sin(x) / x) * sin(xx) / xx; - }; - }, - - /** - * Applies filter to canvas element - * @memberOf fabric.Image.filters.Resize.prototype - * @param {Object} canvasEl Canvas element to apply filter to - * @param {Number} scaleX - * @param {Number} scaleY - */ - applyTo2d: function (options) { - var imageData = options.imageData, - scaleX = this.scaleX, - scaleY = this.scaleY; - - this.rcpScaleX = 1 / scaleX; - this.rcpScaleY = 1 / scaleY; - - var oW = imageData.width, oH = imageData.height, - dW = round(oW * scaleX), dH = round(oH * scaleY), - newData; - - if (this.resizeType === 'sliceHack') { - newData = this.sliceByTwo(options, oW, oH, dW, dH); - } - else if (this.resizeType === 'hermite') { - newData = this.hermiteFastResize(options, oW, oH, dW, dH); - } - else if (this.resizeType === 'bilinear') { - newData = this.bilinearFiltering(options, oW, oH, dW, dH); - } - else if (this.resizeType === 'lanczos') { - newData = this.lanczosResize(options, oW, oH, dW, dH); - } - options.imageData = newData; - }, - - /** - * Filter sliceByTwo - * @param {Object} canvasEl Canvas element to apply filter to - * @param {Number} oW Original Width - * @param {Number} oH Original Height - * @param {Number} dW Destination Width - * @param {Number} dH Destination Height - * @returns {ImageData} - */ - sliceByTwo: function (options, oW, oH, dW, dH) { - var imageData = options.imageData, - mult = 0.5, doneW = false, doneH = false, stepW = oW * mult, - stepH = oH * mult, resources = fabric.filterBackend.resources, - tmpCanvas, ctx, sX = 0, sY = 0, dX = oW, dY = 0; - if (!resources.sliceByTwo) { - resources.sliceByTwo = document.createElement('canvas'); - } - tmpCanvas = resources.sliceByTwo; - if (tmpCanvas.width < oW * 1.5 || tmpCanvas.height < oH) { - tmpCanvas.width = oW * 1.5; - tmpCanvas.height = oH; - } - ctx = tmpCanvas.getContext('2d'); - ctx.clearRect(0, 0, oW * 1.5, oH); - ctx.putImageData(imageData, 0, 0); - - dW = floor(dW); - dH = floor(dH); - - while (!doneW || !doneH) { - oW = stepW; - oH = stepH; - if (dW < floor(stepW * mult)) { - stepW = floor(stepW * mult); - } - else { - stepW = dW; - doneW = true; - } - if (dH < floor(stepH * mult)) { - stepH = floor(stepH * mult); - } - else { - stepH = dH; - doneH = true; - } - ctx.drawImage(tmpCanvas, sX, sY, oW, oH, dX, dY, stepW, stepH); - sX = dX; - sY = dY; - dY += stepH; - } - return ctx.getImageData(sX, sY, dW, dH); - }, - - /** - * Filter lanczosResize - * @param {Object} canvasEl Canvas element to apply filter to - * @param {Number} oW Original Width - * @param {Number} oH Original Height - * @param {Number} dW Destination Width - * @param {Number} dH Destination Height - * @returns {ImageData} - */ - lanczosResize: function (options, oW, oH, dW, dH) { - - function process(u) { - var v, i, weight, idx, a, red, green, - blue, alpha, fX, fY; - center.x = (u + 0.5) * ratioX; - icenter.x = floor(center.x); - for (v = 0; v < dH; v++) { - center.y = (v + 0.5) * ratioY; - icenter.y = floor(center.y); - a = 0; red = 0; green = 0; blue = 0; alpha = 0; - for (i = icenter.x - range2X; i <= icenter.x + range2X; i++) { - if (i < 0 || i >= oW) { - continue; - } - fX = floor(1000 * abs(i - center.x)); - if (!cacheLanc[fX]) { - cacheLanc[fX] = {}; - } - for (var j = icenter.y - range2Y; j <= icenter.y + range2Y; j++) { - if (j < 0 || j >= oH) { - continue; - } - fY = floor(1000 * abs(j - center.y)); - if (!cacheLanc[fX][fY]) { - cacheLanc[fX][fY] = lanczos(sqrt(pow(fX * rcpRatioX, 2) + pow(fY * rcpRatioY, 2)) / 1000); - } - weight = cacheLanc[fX][fY]; - if (weight > 0) { - idx = (j * oW + i) * 4; - a += weight; - red += weight * srcData[idx]; - green += weight * srcData[idx + 1]; - blue += weight * srcData[idx + 2]; - alpha += weight * srcData[idx + 3]; - } - } - } - idx = (v * dW + u) * 4; - destData[idx] = red / a; - destData[idx + 1] = green / a; - destData[idx + 2] = blue / a; - destData[idx + 3] = alpha / a; - } - - if (++u < dW) { - return process(u); - } - else { - return destImg; - } - } - - var srcData = options.imageData.data, - destImg = options.ctx.createImageData(dW, dH), - destData = destImg.data, - lanczos = this.lanczosCreate(this.lanczosLobes), - ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, - rcpRatioX = 2 / this.rcpScaleX, rcpRatioY = 2 / this.rcpScaleY, - range2X = ceil(ratioX * this.lanczosLobes / 2), - range2Y = ceil(ratioY * this.lanczosLobes / 2), - cacheLanc = {}, center = {}, icenter = {}; - - return process(0); - }, - - /** - * bilinearFiltering - * @param {Object} canvasEl Canvas element to apply filter to - * @param {Number} oW Original Width - * @param {Number} oH Original Height - * @param {Number} dW Destination Width - * @param {Number} dH Destination Height - * @returns {ImageData} - */ - bilinearFiltering: function (options, oW, oH, dW, dH) { - var a, b, c, d, x, y, i, j, xDiff, yDiff, chnl, - color, offset = 0, origPix, ratioX = this.rcpScaleX, - ratioY = this.rcpScaleY, - w4 = 4 * (oW - 1), img = options.imageData, - pixels = img.data, destImage = options.ctx.createImageData(dW, dH), - destPixels = destImage.data; - for (i = 0; i < dH; i++) { - for (j = 0; j < dW; j++) { - x = floor(ratioX * j); - y = floor(ratioY * i); - xDiff = ratioX * j - x; - yDiff = ratioY * i - y; - origPix = 4 * (y * oW + x); - - for (chnl = 0; chnl < 4; chnl++) { - a = pixels[origPix + chnl]; - b = pixels[origPix + 4 + chnl]; - c = pixels[origPix + w4 + chnl]; - d = pixels[origPix + w4 + 4 + chnl]; - color = a * (1 - xDiff) * (1 - yDiff) + b * xDiff * (1 - yDiff) + - c * yDiff * (1 - xDiff) + d * xDiff * yDiff; - destPixels[offset++] = color; - } - } - } - return destImage; - }, - - /** - * hermiteFastResize - * @param {Object} canvasEl Canvas element to apply filter to - * @param {Number} oW Original Width - * @param {Number} oH Original Height - * @param {Number} dW Destination Width - * @param {Number} dH Destination Height - * @returns {ImageData} - */ - hermiteFastResize: function (options, oW, oH, dW, dH) { - var ratioW = this.rcpScaleX, ratioH = this.rcpScaleY, - ratioWHalf = ceil(ratioW / 2), - ratioHHalf = ceil(ratioH / 2), - img = options.imageData, data = img.data, - img2 = options.ctx.createImageData(dW, dH), data2 = img2.data; - for (var j = 0; j < dH; j++) { - for (var i = 0; i < dW; i++) { - var x2 = (i + j * dW) * 4, weight = 0, weights = 0, weightsAlpha = 0, - gxR = 0, gxG = 0, gxB = 0, gxA = 0, centerY = (j + 0.5) * ratioH; - for (var yy = floor(j * ratioH); yy < (j + 1) * ratioH; yy++) { - var dy = abs(centerY - (yy + 0.5)) / ratioHHalf, - centerX = (i + 0.5) * ratioW, w0 = dy * dy; - for (var xx = floor(i * ratioW); xx < (i + 1) * ratioW; xx++) { - var dx = abs(centerX - (xx + 0.5)) / ratioWHalf, - w = sqrt(w0 + dx * dx); - /* eslint-disable max-depth */ - if (w > 1 && w < -1) { - continue; - } - //hermite filter - weight = 2 * w * w * w - 3 * w * w + 1; - if (weight > 0) { - dx = 4 * (xx + yy * oW); - //alpha - gxA += weight * data[dx + 3]; - weightsAlpha += weight; - //colors - if (data[dx + 3] < 255) { - weight = weight * data[dx + 3] / 250; - } - gxR += weight * data[dx]; - gxG += weight * data[dx + 1]; - gxB += weight * data[dx + 2]; - weights += weight; - } - /* eslint-enable max-depth */ - } - } - data2[x2] = gxR / weights; - data2[x2 + 1] = gxG / weights; - data2[x2 + 2] = gxB / weights; - data2[x2 + 3] = gxA / weightsAlpha; - } - } - return img2; - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function () { - return { - type: this.type, - scaleX: this.scaleX, - scaleY: this.scaleY, - resizeType: this.resizeType, - lanczosLobes: this.lanczosLobes - }; - } - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize - */ - fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Contrast filter class - * @class fabric.Image.filters.Contrast - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Contrast#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Contrast({ - * contrast: 0.25 - * }); - * object.filters.push(filter); - * object.applyFilters(); - */ - filters.Contrast = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Contrast.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Contrast', - - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uContrast;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'float contrastF = 1.015 * (uContrast + 1.0) / (1.0 * (1.015 - uContrast));\n' + - 'color.rgb = contrastF * (color.rgb - 0.5) + 0.5;\n' + - 'gl_FragColor = color;\n' + - '}', - - /** - * contrast value, range from -1 to 1. - * @param {Number} contrast - * @default 0 - */ - contrast: 0, - - mainParameter: 'contrast', - - /** - * Constructor - * @memberOf fabric.Image.filters.Contrast.prototype - * @param {Object} [options] Options object - * @param {Number} [options.contrast=0] Value to contrast the image up (-1...1) - */ - - /** - * Apply the Contrast operation to a Uint8Array representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8Array to be filtered. - */ - applyTo2d: function (options) { - if (this.contrast === 0) { - return; - } - var imageData = options.imageData, i, len, - data = imageData.data, len = data.length, - contrast = Math.floor(this.contrast * 255), - contrastF = 259 * (contrast + 255) / (255 * (259 - contrast)); - - for (i = 0; i < len; i += 4) { - data[i] = contrastF * (data[i] - 128) + 128; - data[i + 1] = contrastF * (data[i + 1] - 128) + 128; - data[i + 2] = contrastF * (data[i + 2] - 128) + 128; - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uContrast: gl.getUniformLocation(program, 'uContrast'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform1f(uniformLocations.uContrast, this.contrast); - }, - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast - */ - fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Saturate filter class - * @class fabric.Image.filters.Saturation - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Saturation#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Saturation({ - * saturation: 1 - * }); - * object.filters.push(filter); - * object.applyFilters(); - */ - filters.Saturation = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Saturation.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Saturation', - - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uSaturation;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'float rgMax = max(color.r, color.g);\n' + - 'float rgbMax = max(rgMax, color.b);\n' + - 'color.r += rgbMax != color.r ? (rgbMax - color.r) * uSaturation : 0.00;\n' + - 'color.g += rgbMax != color.g ? (rgbMax - color.g) * uSaturation : 0.00;\n' + - 'color.b += rgbMax != color.b ? (rgbMax - color.b) * uSaturation : 0.00;\n' + - 'gl_FragColor = color;\n' + - '}', - - /** - * Saturation value, from -1 to 1. - * Increases/decreases the color saturation. - * A value of 0 has no effect. - * - * @param {Number} saturation - * @default - */ - saturation: 0, - - mainParameter: 'saturation', - - /** - * Constructor - * @memberOf fabric.Image.filters.Saturate.prototype - * @param {Object} [options] Options object - * @param {Number} [options.saturate=0] Value to saturate the image (-1...1) - */ - - /** - * Apply the Saturation operation to a Uint8ClampedArray representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. - */ - applyTo2d: function (options) { - if (this.saturation === 0) { - return; - } - var imageData = options.imageData, - data = imageData.data, len = data.length, - adjust = -this.saturation, i, max; - - for (i = 0; i < len; i += 4) { - max = Math.max(data[i], data[i + 1], data[i + 2]); - data[i] += max !== data[i] ? (max - data[i]) * adjust : 0; - data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * adjust : 0; - data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * adjust : 0; - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uSaturation: gl.getUniformLocation(program, 'uSaturation'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform1f(uniformLocations.uSaturation, -this.saturation); - }, - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate - */ - fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Vibrance filter class - * @class fabric.Image.filters.Vibrance - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Vibrance#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Vibrance({ - * vibrance: 1 - * }); - * object.filters.push(filter); - * object.applyFilters(); - */ - filters.Vibrance = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Vibrance.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Vibrance', - - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform float uVibrance;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'float max = max(color.r, max(color.g, color.b));\n' + - 'float avg = (color.r + color.g + color.b) / 3.0;\n' + - 'float amt = (abs(max - avg) * 2.0) * uVibrance;\n' + - 'color.r += max != color.r ? (max - color.r) * amt : 0.00;\n' + - 'color.g += max != color.g ? (max - color.g) * amt : 0.00;\n' + - 'color.b += max != color.b ? (max - color.b) * amt : 0.00;\n' + - 'gl_FragColor = color;\n' + - '}', - - /** - * Vibrance value, from -1 to 1. - * Increases/decreases the saturation of more muted colors with less effect on saturated colors. - * A value of 0 has no effect. - * - * @param {Number} vibrance - * @default - */ - vibrance: 0, - - mainParameter: 'vibrance', - - /** - * Constructor - * @memberOf fabric.Image.filters.Vibrance.prototype - * @param {Object} [options] Options object - * @param {Number} [options.vibrance=0] Vibrance value for the image (between -1 and 1) - */ - - /** - * Apply the Vibrance operation to a Uint8ClampedArray representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. - */ - applyTo2d: function (options) { - if (this.vibrance === 0) { - return; - } - var imageData = options.imageData, - data = imageData.data, len = data.length, - adjust = -this.vibrance, i, max, avg, amt; - - for (i = 0; i < len; i += 4) { - max = Math.max(data[i], data[i + 1], data[i + 2]); - avg = (data[i] + data[i + 1] + data[i + 2]) / 3; - amt = ((Math.abs(max - avg) * 2 / 255) * adjust); - data[i] += max !== data[i] ? (max - data[i]) * amt : 0; - data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * amt : 0; - data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * amt : 0; - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uVibrance: gl.getUniformLocation(program, 'uVibrance'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform1f(uniformLocations.uVibrance, -this.vibrance); - }, - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Vibrance} Instance of fabric.Image.filters.Vibrance - */ - fabric.Image.filters.Vibrance.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Blur filter class - * @class fabric.Image.filters.Blur - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Blur#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Blur({ - * blur: 0.5 - * }); - * object.filters.push(filter); - * object.applyFilters(); - * canvas.renderAll(); - */ - filters.Blur = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blur.prototype */ { - - type: 'Blur', - - /* - 'gl_FragColor = vec4(0.0);', - 'gl_FragColor += texture2D(texture, vTexCoord + -7 * uDelta)*0.0044299121055113265;', - 'gl_FragColor += texture2D(texture, vTexCoord + -6 * uDelta)*0.00895781211794;', - 'gl_FragColor += texture2D(texture, vTexCoord + -5 * uDelta)*0.0215963866053;', - 'gl_FragColor += texture2D(texture, vTexCoord + -4 * uDelta)*0.0443683338718;', - 'gl_FragColor += texture2D(texture, vTexCoord + -3 * uDelta)*0.0776744219933;', - 'gl_FragColor += texture2D(texture, vTexCoord + -2 * uDelta)*0.115876621105;', - 'gl_FragColor += texture2D(texture, vTexCoord + -1 * uDelta)*0.147308056121;', - 'gl_FragColor += texture2D(texture, vTexCoord )*0.159576912161;', - 'gl_FragColor += texture2D(texture, vTexCoord + 1 * uDelta)*0.147308056121;', - 'gl_FragColor += texture2D(texture, vTexCoord + 2 * uDelta)*0.115876621105;', - 'gl_FragColor += texture2D(texture, vTexCoord + 3 * uDelta)*0.0776744219933;', - 'gl_FragColor += texture2D(texture, vTexCoord + 4 * uDelta)*0.0443683338718;', - 'gl_FragColor += texture2D(texture, vTexCoord + 5 * uDelta)*0.0215963866053;', - 'gl_FragColor += texture2D(texture, vTexCoord + 6 * uDelta)*0.00895781211794;', - 'gl_FragColor += texture2D(texture, vTexCoord + 7 * uDelta)*0.0044299121055113265;', - */ - - /* eslint-disable max-len */ - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform vec2 uDelta;\n' + - 'varying vec2 vTexCoord;\n' + - 'const float nSamples = 15.0;\n' + - 'vec3 v3offset = vec3(12.9898, 78.233, 151.7182);\n' + - 'float random(vec3 scale) {\n' + - /* use the fragment position for a different seed per-pixel */ - 'return fract(sin(dot(gl_FragCoord.xyz, scale)) * 43758.5453);\n' + - '}\n' + - 'void main() {\n' + - 'vec4 color = vec4(0.0);\n' + - 'float total = 0.0;\n' + - 'float offset = random(v3offset);\n' + - 'for (float t = -nSamples; t <= nSamples; t++) {\n' + - 'float percent = (t + offset - 0.5) / nSamples;\n' + - 'float weight = 1.0 - abs(percent);\n' + - 'color += texture2D(uTexture, vTexCoord + uDelta * percent) * weight;\n' + - 'total += weight;\n' + - '}\n' + - 'gl_FragColor = color / total;\n' + - '}', - /* eslint-enable max-len */ - - /** - * blur value, in percentage of image dimensions. - * specific to keep the image blur constant at different resolutions - * range between 0 and 1. - * @type Number - * @default - */ - blur: 0, - - mainParameter: 'blur', - - applyTo: function (options) { - if (options.webgl) { - // this aspectRatio is used to give the same blur to vertical and horizontal - this.aspectRatio = options.sourceWidth / options.sourceHeight; - options.passes++; - this._setupFrameBuffer(options); - this.horizontal = true; - this.applyToWebGL(options); - this._swapTextures(options); - this._setupFrameBuffer(options); - this.horizontal = false; - this.applyToWebGL(options); - this._swapTextures(options); - } - else { - this.applyTo2d(options); - } - }, - - applyTo2d: function (options) { - // paint canvasEl with current image data. - //options.ctx.putImageData(options.imageData, 0, 0); - options.imageData = this.simpleBlur(options); - }, - - simpleBlur: function (options) { - var resources = options.filterBackend.resources, canvas1, canvas2, - width = options.imageData.width, - height = options.imageData.height; - - if (!resources.blurLayer1) { - resources.blurLayer1 = fabric.util.createCanvasElement(); - resources.blurLayer2 = fabric.util.createCanvasElement(); - } - canvas1 = resources.blurLayer1; - canvas2 = resources.blurLayer2; - if (canvas1.width !== width || canvas1.height !== height) { - canvas2.width = canvas1.width = width; - canvas2.height = canvas1.height = height; - } - var ctx1 = canvas1.getContext('2d'), - ctx2 = canvas2.getContext('2d'), - nSamples = 15, - random, percent, j, i, - blur = this.blur * 0.06 * 0.5; - - // load first canvas - ctx1.putImageData(options.imageData, 0, 0); - ctx2.clearRect(0, 0, width, height); - - for (i = -nSamples; i <= nSamples; i++) { - random = (Math.random() - 0.5) / 4; - percent = i / nSamples; - j = blur * percent * width + random; - ctx2.globalAlpha = 1 - Math.abs(percent); - ctx2.drawImage(canvas1, j, random); - ctx1.drawImage(canvas2, 0, 0); - ctx2.globalAlpha = 1; - ctx2.clearRect(0, 0, canvas2.width, canvas2.height); - } - for (i = -nSamples; i <= nSamples; i++) { - random = (Math.random() - 0.5) / 4; - percent = i / nSamples; - j = blur * percent * height + random; - ctx2.globalAlpha = 1 - Math.abs(percent); - ctx2.drawImage(canvas1, random, j); - ctx1.drawImage(canvas2, 0, 0); - ctx2.globalAlpha = 1; - ctx2.clearRect(0, 0, canvas2.width, canvas2.height); - } - options.ctx.drawImage(canvas1, 0, 0); - var newImageData = options.ctx.getImageData(0, 0, canvas1.width, canvas1.height); - ctx1.globalAlpha = 1; - ctx1.clearRect(0, 0, canvas1.width, canvas1.height); - return newImageData; - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - delta: gl.getUniformLocation(program, 'uDelta'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - var delta = this.chooseRightDelta(); - gl.uniform2fv(uniformLocations.delta, delta); - }, - - /** - * choose right value of image percentage to blur with - * @returns {Array} a numeric array with delta values - */ - chooseRightDelta: function () { - var blurScale = 1, delta = [0, 0], blur; - if (this.horizontal) { - if (this.aspectRatio > 1) { - // image is wide, i want to shrink radius horizontal - blurScale = 1 / this.aspectRatio; - } - } - else { - if (this.aspectRatio < 1) { - // image is tall, i want to shrink radius vertical - blurScale = this.aspectRatio; - } - } - blur = blurScale * this.blur * 0.12; - if (this.horizontal) { - delta[0] = blur; - } - else { - delta[1] = blur; - } - return delta; - }, - }); - - /** - * Deserialize a JSON definition of a BlurFilter into a concrete instance. - */ - filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Gamma filter class - * @class fabric.Image.filters.Gamma - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Gamma#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.Gamma({ - * gamma: [1, 0.5, 2.1] - * }); - * object.filters.push(filter); - * object.applyFilters(); - */ - filters.Gamma = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Gamma.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Gamma', - - fragmentSource: 'precision highp float;\n' + - 'uniform sampler2D uTexture;\n' + - 'uniform vec3 uGamma;\n' + - 'varying vec2 vTexCoord;\n' + - 'void main() {\n' + - 'vec4 color = texture2D(uTexture, vTexCoord);\n' + - 'vec3 correction = (1.0 / uGamma);\n' + - 'color.r = pow(color.r, correction.r);\n' + - 'color.g = pow(color.g, correction.g);\n' + - 'color.b = pow(color.b, correction.b);\n' + - 'gl_FragColor = color;\n' + - 'gl_FragColor.rgb *= color.a;\n' + - '}', - - /** - * Gamma array value, from 0.01 to 2.2. - * @param {Array} gamma - * @default - */ - gamma: [1, 1, 1], - - /** - * Describe the property that is the filter parameter - * @param {String} m - * @default - */ - mainParameter: 'gamma', - - /** - * Constructor - * @param {Object} [options] Options object - */ - initialize: function (options) { - this.gamma = [1, 1, 1]; - filters.BaseFilter.prototype.initialize.call(this, options); - }, - - /** - * Apply the Gamma operation to a Uint8Array representing the pixels of an image. - * - * @param {Object} options - * @param {ImageData} options.imageData The Uint8Array to be filtered. - */ - applyTo2d: function (options) { - var imageData = options.imageData, data = imageData.data, - gamma = this.gamma, len = data.length, - rInv = 1 / gamma[0], gInv = 1 / gamma[1], - bInv = 1 / gamma[2], i; - - if (!this.rVals) { - // eslint-disable-next-line - this.rVals = new Uint8Array(256); - // eslint-disable-next-line - this.gVals = new Uint8Array(256); - // eslint-disable-next-line - this.bVals = new Uint8Array(256); - } - - // This is an optimization - pre-compute a look-up table for each color channel - // instead of performing these pow calls for each pixel in the image. - for (i = 0, len = 256; i < len; i++) { - this.rVals[i] = Math.pow(i / 255, rInv) * 255; - this.gVals[i] = Math.pow(i / 255, gInv) * 255; - this.bVals[i] = Math.pow(i / 255, bInv) * 255; - } - for (i = 0, len = data.length; i < len; i += 4) { - data[i] = this.rVals[data[i]]; - data[i + 1] = this.gVals[data[i + 1]]; - data[i + 2] = this.bVals[data[i + 2]]; - } - }, - - /** - * Return WebGL uniform locations for this filter's shader. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {WebGLShaderProgram} program This filter's compiled shader program. - */ - getUniformLocations: function (gl, program) { - return { - uGamma: gl.getUniformLocation(program, 'uGamma'), - }; - }, - - /** - * Send data from this filter to its shader program's uniforms. - * - * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. - * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects - */ - sendUniformData: function (gl, uniformLocations) { - gl.uniform3fv(uniformLocations.uGamma, this.gamma); - }, - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma - */ - fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * A container class that knows how to apply a sequence of filters to an input image. - */ - filters.Composed = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Composed.prototype */ { - - type: 'Composed', - - /** - * A non sparse array of filters to apply - */ - subFilters: [], - - /** - * Constructor - * @param {Object} [options] Options object - */ - initialize: function (options) { - this.callSuper('initialize', options); - // create a new array instead mutating the prototype with push - this.subFilters = this.subFilters.slice(0); - }, - - /** - * Apply this container's filters to the input image provided. - * - * @param {Object} options - * @param {Number} options.passes The number of filters remaining to be applied. - */ - applyTo: function (options) { - options.passes += this.subFilters.length - 1; - this.subFilters.forEach(function (filter) { - filter.applyTo(options); - }); - }, - - /** - * Serialize this filter into JSON. - * - * @returns {Object} A JSON representation of this filter. - */ - toObject: function () { - return fabric.util.object.extend(this.callSuper('toObject'), { - subFilters: this.subFilters.map(function (filter) { return filter.toObject(); }), - }); - }, - - isNeutralState: function () { - return !this.subFilters.some(function (filter) { return !filter.isNeutralState(); }); - } - }); - - /** - * Deserialize a JSON definition of a ComposedFilter into a concrete instance. - */ - fabric.Image.filters.Composed.fromObject = function (object, callback) { - var filters = object.subFilters || [], - subFilters = filters.map(function (filter) { - return new fabric.Image.filters[filter.type](filter); - }), - instance = new fabric.Image.filters.Composed({ subFilters: subFilters }); - callback && callback(instance); - return instance; - }; -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * HueRotation filter class - * @class fabric.Image.filters.HueRotation - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.HueRotation#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.HueRotation({ - * rotation: -0.5 - * }); - * object.filters.push(filter); - * object.applyFilters(); - */ - filters.HueRotation = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.HueRotation.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'HueRotation', - - /** - * HueRotation value, from -1 to 1. - * the unit is radians - * @param {Number} myParameter - * @default - */ - rotation: 0, - - /** - * Describe the property that is the filter parameter - * @param {String} m - * @default - */ - mainParameter: 'rotation', - - calculateMatrix: function () { - var rad = this.rotation * Math.PI, cos = fabric.util.cos(rad), sin = fabric.util.sin(rad), - aThird = 1 / 3, aThirdSqtSin = Math.sqrt(aThird) * sin, OneMinusCos = 1 - cos; - this.matrix = [ - 1, 0, 0, 0, 0, - 0, 1, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 1, 0 - ]; - this.matrix[0] = cos + OneMinusCos / 3; - this.matrix[1] = aThird * OneMinusCos - aThirdSqtSin; - this.matrix[2] = aThird * OneMinusCos + aThirdSqtSin; - this.matrix[5] = aThird * OneMinusCos + aThirdSqtSin; - this.matrix[6] = cos + aThird * OneMinusCos; - this.matrix[7] = aThird * OneMinusCos - aThirdSqtSin; - this.matrix[10] = aThird * OneMinusCos - aThirdSqtSin; - this.matrix[11] = aThird * OneMinusCos + aThirdSqtSin; - this.matrix[12] = cos + aThird * OneMinusCos; - }, - - /** - * HueRotation isNeutralState implementation - * Used only in image applyFilters to discard filters that will not have an effect - * on the image - * @param {Object} options - **/ - isNeutralState: function (options) { - this.calculateMatrix(); - return filters.BaseFilter.prototype.isNeutralState.call(this, options); - }, - - /** - * Apply this filter to the input image data provided. - * - * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. - * - * @param {Object} options - * @param {Number} options.passes The number of filters remaining to be executed - * @param {Boolean} options.webgl Whether to use webgl to render the filter. - * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. - * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. - * @param {WebGLRenderingContext} options.context The GL context used for rendering. - * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. - */ - applyTo: function (options) { - this.calculateMatrix(); - filters.BaseFilter.prototype.applyTo.call(this, options); - }, - - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation - */ - fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}), - clone = fabric.util.object.clone; - - if (fabric.Text) { - fabric.warn('fabric.Text is already defined'); - return; - } - - var additionalProps = - ('fontFamily fontWeight fontSize text underline overline linethrough' + - ' textAlign fontStyle lineHeight textBackgroundColor charSpacing styles' + - ' direction path pathStartOffset pathSide pathAlign').split(' '); - - /** - * Text class - * @class fabric.Text - * @extends fabric.Object - * @return {fabric.Text} thisArg - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text} - * @see {@link fabric.Text#initialize} for constructor definition - */ - fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { - - /** - * Properties which when set cause object to change dimensions - * @type Array - * @private - */ - _dimensionAffectingProps: [ - 'fontSize', - 'fontWeight', - 'fontFamily', - 'fontStyle', - 'lineHeight', - 'text', - 'charSpacing', - 'textAlign', - 'styles', - 'path', - 'pathStartOffset', - 'pathSide', - 'pathAlign' - ], - - /** - * @private - */ - _reNewline: /\r?\n/, - - /** - * Use this regular expression to filter for whitespaces that is not a new line. - * Mostly used when text is 'justify' aligned. - * @private - */ - _reSpacesAndTabs: /[ \t\r]/g, - - /** - * Use this regular expression to filter for whitespace that is not a new line. - * Mostly used when text is 'justify' aligned. - * @private - */ - _reSpaceAndTab: /[ \t\r]/, - - /** - * Use this regular expression to filter consecutive groups of non spaces. - * Mostly used when text is 'justify' aligned. - * @private - */ - _reWords: /\S+/g, - - /** - * Type of an object - * @type String - * @default - */ - type: 'text', - - /** - * Font size (in pixels) - * @type Number - * @default - */ - fontSize: 40, - - /** - * Font weight (e.g. bold, normal, 400, 600, 800) - * @type {(Number|String)} - * @default - */ - fontWeight: 'normal', - - /** - * Font family - * @type String - * @default - */ - fontFamily: 'Times New Roman', - - /** - * Text decoration underline. - * @type Boolean - * @default - */ - underline: false, - - /** - * Text decoration overline. - * @type Boolean - * @default - */ - overline: false, - - /** - * Text decoration linethrough. - * @type Boolean - * @default - */ - linethrough: false, - - /** - * Text alignment. Possible values: "left", "center", "right", "justify", - * "justify-left", "justify-center" or "justify-right". - * @type String - * @default - */ - textAlign: 'left', - - /** - * Font style . Possible values: "", "normal", "italic" or "oblique". - * @type String - * @default - */ - fontStyle: 'normal', - - /** - * Line height - * @type Number - * @default - */ - lineHeight: 1.16, - - /** - * Superscript schema object (minimum overlap) - * @type {Object} - * @default - */ - superscript: { - size: 0.60, // fontSize factor - baseline: -0.35 // baseline-shift factor (upwards) - }, - - /** - * Subscript schema object (minimum overlap) - * @type {Object} - * @default - */ - subscript: { - size: 0.60, // fontSize factor - baseline: 0.11 // baseline-shift factor (downwards) - }, - - /** - * Background color of text lines - * @type String - * @default - */ - textBackgroundColor: '', - - /** - * List of properties to consider when checking if - * state of an object is changed ({@link fabric.Object#hasStateChanged}) - * as well as for history (undo/redo) purposes - * @type Array - */ - stateProperties: fabric.Object.prototype.stateProperties.concat(additionalProps), - - /** - * List of properties to consider when checking if cache needs refresh - * @type Array - */ - cacheProperties: fabric.Object.prototype.cacheProperties.concat(additionalProps), - - /** - * When defined, an object is rendered via stroke and this property specifies its color. - * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6 - * @type String - * @default - */ - stroke: null, - - /** - * Shadow object representing shadow of this shape. - * Backwards incompatibility note: This property was named "textShadow" (String) until v1.2.11 - * @type fabric.Shadow - * @default - */ - shadow: null, - - /** - * fabric.Path that the text should follow. - * since 4.6.0 the path will be drawn automatically. - * if you want to make the path visible, give it a stroke and strokeWidth or fill value - * if you want it to be hidden, assign visible = false to the path. - * This feature is in BETA, and SVG import/export is not yet supported. - * @type fabric.Path - * @example - * var textPath = new fabric.Text('Text on a path', { - * top: 150, - * left: 150, - * textAlign: 'center', - * charSpacing: -50, - * path: new fabric.Path('M 0 0 C 50 -100 150 -100 200 0', { - * strokeWidth: 1, - * visible: false - * }), - * pathSide: 'left', - * pathStartOffset: 0 - * }); - * @default - */ - path: null, - - /** - * Offset amount for text path starting position - * Only used when text has a path - * @type Number - * @default - */ - pathStartOffset: 0, - - /** - * Which side of the path the text should be drawn on. - * Only used when text has a path - * @type {String} 'left|right' - * @default - */ - pathSide: 'left', - - /** - * How text is aligned to the path. This property determines - * the perpendicular position of each character relative to the path. - * (one of "baseline", "center", "ascender", "descender") - * This feature is in BETA, and its behavior may change - * @type String - * @default - */ - pathAlign: 'baseline', - - /** - * @private - */ - _fontSizeFraction: 0.222, - - /** - * @private - */ - offsets: { - underline: 0.10, - linethrough: -0.315, - overline: -0.88 - }, - - /** - * Text Line proportion to font Size (in pixels) - * @type Number - * @default - */ - _fontSizeMult: 1.13, - - /** - * additional space between characters - * expressed in thousands of em unit - * @type Number - * @default - */ - charSpacing: 0, - - /** - * Object containing character styles - top-level properties -> line numbers, - * 2nd-level properties - character numbers - * @type Object - * @default - */ - styles: null, - - /** - * Reference to a context to measure text char or couple of chars - * the cacheContext of the canvas will be used or a freshly created one if the object is not on canvas - * once created it will be referenced on fabric._measuringContext to avoid creating a canvas for every - * text object created. - * @type {CanvasRenderingContext2D} - * @default - */ - _measuringContext: null, - - /** - * Baseline shift, styles only, keep at 0 for the main text object - * @type {Number} - * @default - */ - deltaY: 0, - - /** - * WARNING: EXPERIMENTAL. NOT SUPPORTED YET - * determine the direction of the text. - * This has to be set manually together with textAlign and originX for proper - * experience. - * some interesting link for the future - * https://www.w3.org/International/questions/qa-bidi-unicode-controls - * @since 4.5.0 - * @type {String} 'ltr|rtl' - * @default - */ - direction: 'ltr', - - /** - * Array of properties that define a style unit (of 'styles'). - * @type {Array} - * @default - */ - _styleProperties: [ - 'stroke', - 'strokeWidth', - 'fill', - 'fontFamily', - 'fontSize', - 'fontWeight', - 'fontStyle', - 'underline', - 'overline', - 'linethrough', - 'deltaY', - 'textBackgroundColor', - ], - - /** - * contains characters bounding boxes - */ - __charBounds: [], - - /** - * use this size when measuring text. To avoid IE11 rounding errors - * @type {Number} - * @default - * @readonly - * @private - */ - CACHE_FONT_SIZE: 400, - - /** - * contains the min text width to avoid getting 0 - * @type {Number} - * @default - */ - MIN_TEXT_WIDTH: 2, - - /** - * Constructor - * @param {String} text Text string - * @param {Object} [options] Options object - * @return {fabric.Text} thisArg - */ - initialize: function (text, options) { - this.styles = options ? (options.styles || {}) : {}; - this.text = text; - this.__skipDimension = true; - this.callSuper('initialize', options); - if (this.path) { - this.setPathInfo(); - } - this.__skipDimension = false; - this.initDimensions(); - this.setCoords(); - this.setupState({ propertySet: '_dimensionAffectingProps' }); - }, - - /** - * If text has a path, it will add the extra information needed - * for path and text calculations - * @return {fabric.Text} thisArg - */ - setPathInfo: function () { - var path = this.path; - if (path) { - path.segmentsInfo = fabric.util.getPathSegmentsInfo(path.path); - } - }, - - /** - * Return a context for measurement of text string. - * if created it gets stored for reuse - * this is for internal use, please do not use it - * @private - * @param {String} text Text string - * @param {Object} [options] Options object - * @return {fabric.Text} thisArg - */ - getMeasuringContext: function () { - // if we did not return we have to measure something. - if (!fabric._measuringContext) { - fabric._measuringContext = this.canvas && this.canvas.contextCache || - fabric.util.createCanvasElement().getContext('2d'); - } - return fabric._measuringContext; - }, - - /** - * @private - * Divides text into lines of text and lines of graphemes. - */ - _splitText: function () { - var newLines = this._splitTextIntoLines(this.text); - this.textLines = newLines.lines; - this._textLines = newLines.graphemeLines; - this._unwrappedTextLines = newLines._unwrappedLines; - this._text = newLines.graphemeText; - return newLines; - }, - - /** - * Initialize or update text dimensions. - * Updates this.width and this.height with the proper values. - * Does not return dimensions. - */ - initDimensions: function () { - if (this.__skipDimension) { - return; - } - this._splitText(); - this._clearCache(); - if (this.path) { - this.width = this.path.width; - this.height = this.path.height; - } - else { - this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH; - this.height = this.calcTextHeight(); - } - if (this.textAlign.indexOf('justify') !== -1) { - // once text is measured we need to make space fatter to make justified text. - this.enlargeSpaces(); - } - this.saveState({ propertySet: '_dimensionAffectingProps' }); - }, - - /** - * Enlarge space boxes and shift the others - */ - enlargeSpaces: function () { - var diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces; - for (var i = 0, len = this._textLines.length; i < len; i++) { - if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) { - continue; - } - accumulatedSpace = 0; - line = this._textLines[i]; - currentLineWidth = this.getLineWidth(i); - if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) { - numberOfSpaces = spaces.length; - diffSpace = (this.width - currentLineWidth) / numberOfSpaces; - for (var j = 0, jlen = line.length; j <= jlen; j++) { - charBound = this.__charBounds[i][j]; - if (this._reSpaceAndTab.test(line[j])) { - charBound.width += diffSpace; - charBound.kernedWidth += diffSpace; - charBound.left += accumulatedSpace; - accumulatedSpace += diffSpace; - } - else { - charBound.left += accumulatedSpace; - } - } - } - } - }, - - /** - * Detect if the text line is ended with an hard break - * text and itext do not have wrapping, return false - * @return {Boolean} - */ - isEndOfWrapping: function (lineIndex) { - return lineIndex === this._textLines.length - 1; - }, - - /** - * Detect if a line has a linebreak and so we need to account for it when moving - * and counting style. - * It return always for text and Itext. - * @return Number - */ - missingNewlineOffset: function () { - return 1; - }, - - /** - * Returns string representation of an instance - * @return {String} String representation of text object - */ - toString: function () { - return '#'; - }, - - /** - * Return the dimension and the zoom level needed to create a cache canvas - * big enough to host the object to be cached. - * @private - * @param {Object} dim.x width of object to be cached - * @param {Object} dim.y height of object to be cached - * @return {Object}.width width of canvas - * @return {Object}.height height of canvas - * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache - * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache - */ - _getCacheCanvasDimensions: function () { - var dims = this.callSuper('_getCacheCanvasDimensions'); - var fontSize = this.fontSize; - dims.width += fontSize * dims.zoomX; - dims.height += fontSize * dims.zoomY; - return dims; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - var path = this.path; - path && !path.isNotVisible() && path._render(ctx); - this._setTextStyles(ctx); - this._renderTextLinesBackground(ctx); - this._renderTextDecoration(ctx, 'underline'); - this._renderText(ctx); - this._renderTextDecoration(ctx, 'overline'); - this._renderTextDecoration(ctx, 'linethrough'); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderText: function (ctx) { - if (this.paintFirst === 'stroke') { - this._renderTextStroke(ctx); - this._renderTextFill(ctx); - } - else { - this._renderTextFill(ctx); - this._renderTextStroke(ctx); - } - }, - - /** - * Set the font parameter of the context with the object properties or with charStyle - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Object} [charStyle] object with font style properties - * @param {String} [charStyle.fontFamily] Font Family - * @param {Number} [charStyle.fontSize] Font size in pixels. ( without px suffix ) - * @param {String} [charStyle.fontWeight] Font weight - * @param {String} [charStyle.fontStyle] Font style (italic|normal) - */ - _setTextStyles: function (ctx, charStyle, forMeasuring) { - ctx.textBaseline = 'alphabetical'; - if (this.path) { - switch (this.pathAlign) { - case 'center': - ctx.textBaseline = 'middle'; - break; - case 'ascender': - ctx.textBaseline = 'top'; - break; - case 'descender': - ctx.textBaseline = 'bottom'; - break; - } - } - ctx.font = this._getFontDeclaration(charStyle, forMeasuring); - }, - - /** - * calculate and return the text Width measuring each line. - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @return {Number} Maximum width of fabric.Text object - */ - calcTextWidth: function () { - var maxWidth = this.getLineWidth(0); - - for (var i = 1, len = this._textLines.length; i < len; i++) { - var currentLineWidth = this.getLineWidth(i); - if (currentLineWidth > maxWidth) { - maxWidth = currentLineWidth; - } - } - return maxWidth; - }, - - /** - * @private - * @param {String} method Method name ("fillText" or "strokeText") - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {String} line Text to render - * @param {Number} left Left position of text - * @param {Number} top Top position of text - * @param {Number} lineIndex Index of a line in a text - */ - _renderTextLine: function (method, ctx, line, left, top, lineIndex) { - this._renderChars(method, ctx, line, left, top, lineIndex); - }, - - /** - * Renders the text background for lines, taking care of style - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderTextLinesBackground: function (ctx) { - if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) { - return; - } - var heightOfLine, - lineLeftOffset, originalFill = ctx.fillStyle, - line, lastColor, - leftOffset = this._getLeftOffset(), - lineTopOffset = this._getTopOffset(), - boxStart = 0, boxWidth = 0, charBox, currentColor, path = this.path, - drawStart; - - for (var i = 0, len = this._textLines.length; i < len; i++) { - heightOfLine = this.getHeightOfLine(i); - if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) { - lineTopOffset += heightOfLine; - continue; - } - line = this._textLines[i]; - lineLeftOffset = this._getLineLeftOffset(i); - boxWidth = 0; - boxStart = 0; - lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); - for (var j = 0, jlen = line.length; j < jlen; j++) { - charBox = this.__charBounds[i][j]; - currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); - if (path) { - ctx.save(); - ctx.translate(charBox.renderLeft, charBox.renderTop); - ctx.rotate(charBox.angle); - ctx.fillStyle = currentColor; - currentColor && ctx.fillRect( - -charBox.width / 2, - -heightOfLine / this.lineHeight * (1 - this._fontSizeFraction), - charBox.width, - heightOfLine / this.lineHeight - ); - ctx.restore(); - } - else if (currentColor !== lastColor) { - drawStart = leftOffset + lineLeftOffset + boxStart; - if (this.direction === 'rtl') { - drawStart = this.width - drawStart - boxWidth; - } - ctx.fillStyle = lastColor; - lastColor && ctx.fillRect( - drawStart, - lineTopOffset, - boxWidth, - heightOfLine / this.lineHeight - ); - boxStart = charBox.left; - boxWidth = charBox.width; - lastColor = currentColor; - } - else { - boxWidth += charBox.kernedWidth; - } - } - if (currentColor && !path) { - drawStart = leftOffset + lineLeftOffset + boxStart; - if (this.direction === 'rtl') { - drawStart = this.width - drawStart - boxWidth; - } - ctx.fillStyle = currentColor; - ctx.fillRect( - drawStart, - lineTopOffset, - boxWidth, - heightOfLine / this.lineHeight - ); - } - lineTopOffset += heightOfLine; - } - ctx.fillStyle = originalFill; - // if there is text background color no - // other shadows should be casted - this._removeShadow(ctx); - }, - - /** - * @private - * @param {Object} decl style declaration for cache - * @param {String} decl.fontFamily fontFamily - * @param {String} decl.fontStyle fontStyle - * @param {String} decl.fontWeight fontWeight - * @return {Object} reference to cache - */ - getFontCache: function (decl) { - var fontFamily = decl.fontFamily.toLowerCase(); - if (!fabric.charWidthsCache[fontFamily]) { - fabric.charWidthsCache[fontFamily] = {}; - } - var cache = fabric.charWidthsCache[fontFamily], - cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase(); - if (!cache[cacheProp]) { - cache[cacheProp] = {}; - } - return cache[cacheProp]; - }, - - /** - * measure and return the width of a single character. - * possibly overridden to accommodate different measure logic or - * to hook some external lib for character measurement - * @private - * @param {String} _char, char to be measured - * @param {Object} charStyle style of char to be measured - * @param {String} [previousChar] previous char - * @param {Object} [prevCharStyle] style of previous char - */ - _measureChar: function (_char, charStyle, previousChar, prevCharStyle) { - // first i try to return from cache - var fontCache = this.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle), - previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char, - stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth, - fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE, kernedWidth; - - if (previousChar && fontCache[previousChar] !== undefined) { - previousWidth = fontCache[previousChar]; - } - if (fontCache[_char] !== undefined) { - kernedWidth = width = fontCache[_char]; - } - if (stylesAreEqual && fontCache[couple] !== undefined) { - coupleWidth = fontCache[couple]; - kernedWidth = coupleWidth - previousWidth; - } - if (width === undefined || previousWidth === undefined || coupleWidth === undefined) { - var ctx = this.getMeasuringContext(); - // send a TRUE to specify measuring font size CACHE_FONT_SIZE - this._setTextStyles(ctx, charStyle, true); - } - if (width === undefined) { - kernedWidth = width = ctx.measureText(_char).width; - fontCache[_char] = width; - } - if (previousWidth === undefined && stylesAreEqual && previousChar) { - previousWidth = ctx.measureText(previousChar).width; - fontCache[previousChar] = previousWidth; - } - if (stylesAreEqual && coupleWidth === undefined) { - // we can measure the kerning couple and subtract the width of the previous character - coupleWidth = ctx.measureText(couple).width; - fontCache[couple] = coupleWidth; - kernedWidth = coupleWidth - previousWidth; - } - return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier }; - }, - - /** - * Computes height of character at given position - * @param {Number} line the line index number - * @param {Number} _char the character index number - * @return {Number} fontSize of the character - */ - getHeightOfChar: function (line, _char) { - return this.getValueOfPropertyAt(line, _char, 'fontSize'); - }, - - /** - * measure a text line measuring all characters. - * @param {Number} lineIndex line number - * @return {Number} Line width - */ - measureLine: function (lineIndex) { - var lineInfo = this._measureLine(lineIndex); - if (this.charSpacing !== 0) { - lineInfo.width -= this._getWidthOfCharSpacing(); - } - if (lineInfo.width < 0) { - lineInfo.width = 0; - } - return lineInfo; - }, - - /** - * measure every grapheme of a line, populating __charBounds - * @param {Number} lineIndex - * @return {Object} object.width total width of characters - * @return {Object} object.widthOfSpaces length of chars that match this._reSpacesAndTabs - */ - _measureLine: function (lineIndex) { - var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme, - graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length), - positionInPath = 0, startingPoint, totalPathLength, path = this.path, - reverse = this.pathSide === 'right'; - - this.__charBounds[lineIndex] = lineBounds; - for (i = 0; i < line.length; i++) { - grapheme = line[i]; - graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme); - lineBounds[i] = graphemeInfo; - width += graphemeInfo.kernedWidth; - prevGrapheme = grapheme; - } - // this latest bound box represent the last character of the line - // to simplify cursor handling in interactive mode. - lineBounds[i] = { - left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0, - width: 0, - kernedWidth: 0, - height: this.fontSize - }; - if (path) { - totalPathLength = path.segmentsInfo[path.segmentsInfo.length - 1].length; - startingPoint = fabric.util.getPointOnPath(path.path, 0, path.segmentsInfo); - startingPoint.x += path.pathOffset.x; - startingPoint.y += path.pathOffset.y; - switch (this.textAlign) { - case 'left': - positionInPath = reverse ? (totalPathLength - width) : 0; - break; - case 'center': - positionInPath = (totalPathLength - width) / 2; - break; - case 'right': - positionInPath = reverse ? 0 : (totalPathLength - width); - break; - //todo - add support for justify - } - positionInPath += this.pathStartOffset * (reverse ? -1 : 1); - for (i = reverse ? line.length - 1 : 0; - reverse ? i >= 0 : i < line.length; - reverse ? i-- : i++) { - graphemeInfo = lineBounds[i]; - if (positionInPath > totalPathLength) { - positionInPath %= totalPathLength; - } - else if (positionInPath < 0) { - positionInPath += totalPathLength; - } - // it would probably much faster to send all the grapheme position for a line - // and calculate path position/angle at once. - this._setGraphemeOnPath(positionInPath, graphemeInfo, startingPoint); - positionInPath += graphemeInfo.kernedWidth; - } - } - return { width: width, numOfSpaces: numOfSpaces }; - }, - - /** - * Calculate the angle and the left,top position of the char that follow a path. - * It appends it to graphemeInfo to be reused later at rendering - * @private - * @param {Number} positionInPath to be measured - * @param {Object} graphemeInfo current grapheme box information - * @param {Object} startingPoint position of the point - */ - _setGraphemeOnPath: function (positionInPath, graphemeInfo, startingPoint) { - var centerPosition = positionInPath + graphemeInfo.kernedWidth / 2, - path = this.path; - - // we are at currentPositionOnPath. we want to know what point on the path is. - var info = fabric.util.getPointOnPath(path.path, centerPosition, path.segmentsInfo); - graphemeInfo.renderLeft = info.x - startingPoint.x; - graphemeInfo.renderTop = info.y - startingPoint.y; - graphemeInfo.angle = info.angle + (this.pathSide === 'right' ? Math.PI : 0); - }, - - /** - * Measure and return the info of a single grapheme. - * needs the the info of previous graphemes already filled - * @private - * @param {String} grapheme to be measured - * @param {Number} lineIndex index of the line where the char is - * @param {Number} charIndex position in the line - * @param {String} [prevGrapheme] character preceding the one to be measured - */ - _getGraphemeBox: function (grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { - var style = this.getCompleteStyleDeclaration(lineIndex, charIndex), - prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : {}, - info = this._measureChar(grapheme, style, prevGrapheme, prevStyle), - kernedWidth = info.kernedWidth, - width = info.width, charSpacing; - - if (this.charSpacing !== 0) { - charSpacing = this._getWidthOfCharSpacing(); - width += charSpacing; - kernedWidth += charSpacing; - } - - var box = { - width: width, - left: 0, - height: style.fontSize, - kernedWidth: kernedWidth, - deltaY: style.deltaY, - }; - if (charIndex > 0 && !skipLeft) { - var previousBox = this.__charBounds[lineIndex][charIndex - 1]; - box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width; - } - return box; - }, - - /** - * Calculate height of line at 'lineIndex' - * @param {Number} lineIndex index of line to calculate - * @return {Number} - */ - getHeightOfLine: function (lineIndex) { - if (this.__lineHeights[lineIndex]) { - return this.__lineHeights[lineIndex]; - } - - var line = this._textLines[lineIndex], - // char 0 is measured before the line cycle because it nneds to char - // emptylines - maxHeight = this.getHeightOfChar(lineIndex, 0); - for (var i = 1, len = line.length; i < len; i++) { - maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight); - } - - return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; - }, - - /** - * Calculate text box height - */ - calcTextHeight: function () { - var lineHeight, height = 0; - for (var i = 0, len = this._textLines.length; i < len; i++) { - lineHeight = this.getHeightOfLine(i); - height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight); - } - return height; - }, - - /** - * @private - * @return {Number} Left offset - */ - _getLeftOffset: function () { - return this.direction === 'ltr' ? -this.width / 2 : this.width / 2; - }, - - /** - * @private - * @return {Number} Top offset - */ - _getTopOffset: function () { - return -this.height / 2; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {String} method Method name ("fillText" or "strokeText") - */ - _renderTextCommon: function (ctx, method) { - ctx.save(); - var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(); - for (var i = 0, len = this._textLines.length; i < len; i++) { - var heightOfLine = this.getHeightOfLine(i), - maxHeight = heightOfLine / this.lineHeight, - leftOffset = this._getLineLeftOffset(i); - this._renderTextLine( - method, - ctx, - this._textLines[i], - left + leftOffset, - top + lineHeights + maxHeight, - i - ); - lineHeights += heightOfLine; - } - ctx.restore(); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderTextFill: function (ctx) { - if (!this.fill && !this.styleHas('fill')) { - return; - } - - this._renderTextCommon(ctx, 'fillText'); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderTextStroke: function (ctx) { - if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) { - return; - } - - if (this.shadow && !this.shadow.affectStroke) { - this._removeShadow(ctx); - } - - ctx.save(); - this._setLineDash(ctx, this.strokeDashArray); - ctx.beginPath(); - this._renderTextCommon(ctx, 'strokeText'); - ctx.closePath(); - ctx.restore(); - }, - - /** - * @private - * @param {String} method fillText or strokeText. - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} line Content of the line, splitted in an array by grapheme - * @param {Number} left - * @param {Number} top - * @param {Number} lineIndex - */ - _renderChars: function (method, ctx, line, left, top, lineIndex) { - // set proper line offset - var lineHeight = this.getHeightOfLine(lineIndex), - isJustify = this.textAlign.indexOf('justify') !== -1, - actualStyle, - nextStyle, - charsToRender = '', - charBox, - boxWidth = 0, - timeToRender, - path = this.path, - shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, - isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, - drawingLeft, currentDirection = ctx.canvas.getAttribute('dir'); - ctx.save(); - if (currentDirection !== this.direction) { - ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); - ctx.direction = isLtr ? 'ltr' : 'rtl'; - ctx.textAlign = isLtr ? 'left' : 'right'; - } - top -= lineHeight * this._fontSizeFraction / this.lineHeight; - if (shortCut) { - // render all the line in one pass without checking - // drawingLeft = isLtr ? left : left - this.getLineWidth(lineIndex); - this._renderChar(method, ctx, lineIndex, 0, line.join(''), left, top, lineHeight); - ctx.restore(); - return; - } - for (var i = 0, len = line.length - 1; i <= len; i++) { - timeToRender = i === len || this.charSpacing || path; - charsToRender += line[i]; - charBox = this.__charBounds[lineIndex][i]; - if (boxWidth === 0) { - left += sign * (charBox.kernedWidth - charBox.width); - boxWidth += charBox.width; - } - else { - boxWidth += charBox.kernedWidth; - } - if (isJustify && !timeToRender) { - if (this._reSpaceAndTab.test(line[i])) { - timeToRender = true; - } - } - if (!timeToRender) { - // if we have charSpacing, we render char by char - actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); - nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); - timeToRender = fabric.util.hasStyleChanged(actualStyle, nextStyle, false); - } - if (timeToRender) { - if (path) { - ctx.save(); - ctx.translate(charBox.renderLeft, charBox.renderTop); - ctx.rotate(charBox.angle); - this._renderChar(method, ctx, lineIndex, i, charsToRender, -boxWidth / 2, 0, lineHeight); - ctx.restore(); - } - else { - drawingLeft = left; - this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top, lineHeight); - } - charsToRender = ''; - actualStyle = nextStyle; - left += sign * boxWidth; - boxWidth = 0; - } - } - ctx.restore(); - }, - - /** - * This function try to patch the missing gradientTransform on canvas gradients. - * transforming a context to transform the gradient, is going to transform the stroke too. - * we want to transform the gradient but not the stroke operation, so we create - * a transformed gradient on a pattern and then we use the pattern instead of the gradient. - * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size - * is limited. - * @private - * @param {fabric.Gradient} filler a fabric gradient instance - * @return {CanvasPattern} a pattern to use as fill/stroke style - */ - _applyPatternGradientTransformText: function (filler) { - var pCanvas = fabric.util.createCanvasElement(), pCtx, - // TODO: verify compatibility with strokeUniform - width = this.width + this.strokeWidth, height = this.height + this.strokeWidth; - pCanvas.width = width; - pCanvas.height = height; - pCtx = pCanvas.getContext('2d'); - pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); - pCtx.lineTo(0, height); pCtx.closePath(); - pCtx.translate(width / 2, height / 2); - pCtx.fillStyle = filler.toLive(pCtx); - this._applyPatternGradientTransform(pCtx, filler); - pCtx.fill(); - return pCtx.createPattern(pCanvas, 'no-repeat'); - }, - - handleFiller: function (ctx, property, filler) { - var offsetX, offsetY; - if (filler.toLive) { - if (filler.gradientUnits === 'percentage' || filler.gradientTransform || filler.patternTransform) { - // need to transform gradient in a pattern. - // this is a slow process. If you are hitting this codepath, and the object - // is not using caching, you should consider switching it on. - // we need a canvas as big as the current object caching canvas. - offsetX = -this.width / 2; - offsetY = -this.height / 2; - ctx.translate(offsetX, offsetY); - ctx[property] = this._applyPatternGradientTransformText(filler); - return { offsetX: offsetX, offsetY: offsetY }; - } - else { - // is a simple gradient or pattern - ctx[property] = filler.toLive(ctx, this); - return this._applyPatternGradientTransform(ctx, filler); - } - } - else { - // is a color - ctx[property] = filler; - } - return { offsetX: 0, offsetY: 0 }; - }, - - _setStrokeStyles: function (ctx, decl) { - ctx.lineWidth = decl.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineDashOffset = this.strokeDashOffset; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - return this.handleFiller(ctx, 'strokeStyle', decl.stroke); - }, - - _setFillStyles: function (ctx, decl) { - return this.handleFiller(ctx, 'fillStyle', decl.fill); - }, - - /** - * @private - * @param {String} method - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Number} lineIndex - * @param {Number} charIndex - * @param {String} _char - * @param {Number} left Left coordinate - * @param {Number} top Top coordinate - * @param {Number} lineHeight Height of the line - */ - _renderChar: function (method, ctx, lineIndex, charIndex, _char, left, top) { - var decl = this._getStyleDeclaration(lineIndex, charIndex), - fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex), - shouldFill = method === 'fillText' && fullDecl.fill, - shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth, - fillOffsets, strokeOffsets; - - if (!shouldStroke && !shouldFill) { - return; - } - ctx.save(); - - shouldFill && (fillOffsets = this._setFillStyles(ctx, fullDecl)); - shouldStroke && (strokeOffsets = this._setStrokeStyles(ctx, fullDecl)); - - ctx.font = this._getFontDeclaration(fullDecl); - - - if (decl && decl.textBackgroundColor) { - this._removeShadow(ctx); - } - if (decl && decl.deltaY) { - top += decl.deltaY; - } - shouldFill && ctx.fillText(_char, left - fillOffsets.offsetX, top - fillOffsets.offsetY); - shouldStroke && ctx.strokeText(_char, left - strokeOffsets.offsetX, top - strokeOffsets.offsetY); - ctx.restore(); - }, - - /** - * Turns the character into a 'superior figure' (i.e. 'superscript') - * @param {Number} start selection start - * @param {Number} end selection end - * @returns {fabric.Text} thisArg - * @chainable - */ - setSuperscript: function (start, end) { - return this._setScript(start, end, this.superscript); - }, - - /** - * Turns the character into an 'inferior figure' (i.e. 'subscript') - * @param {Number} start selection start - * @param {Number} end selection end - * @returns {fabric.Text} thisArg - * @chainable - */ - setSubscript: function (start, end) { - return this._setScript(start, end, this.subscript); - }, - - /** - * Applies 'schema' at given position - * @private - * @param {Number} start selection start - * @param {Number} end selection end - * @param {Number} schema - * @returns {fabric.Text} thisArg - * @chainable - */ - _setScript: function (start, end, schema) { - var loc = this.get2DCursorLocation(start, true), - fontSize = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'fontSize'), - dy = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'deltaY'), - style = { fontSize: fontSize * schema.size, deltaY: dy + fontSize * schema.baseline }; - this.setSelectionStyles(style, start, end); - return this; - }, - - /** - * @private - * @param {Number} lineIndex index text line - * @return {Number} Line left offset - */ - _getLineLeftOffset: function (lineIndex) { - var lineWidth = this.getLineWidth(lineIndex), - lineDiff = this.width - lineWidth, textAlign = this.textAlign, direction = this.direction, - isEndOfWrapping, leftOffset = 0, isEndOfWrapping = this.isEndOfWrapping(lineIndex); - if (textAlign === 'justify' - || (textAlign === 'justify-center' && !isEndOfWrapping) - || (textAlign === 'justify-right' && !isEndOfWrapping) - || (textAlign === 'justify-left' && !isEndOfWrapping) - ) { - return 0; - } - if (textAlign === 'center') { - leftOffset = lineDiff / 2; - } - if (textAlign === 'right') { - leftOffset = lineDiff; - } - if (textAlign === 'justify-center') { - leftOffset = lineDiff / 2; - } - if (textAlign === 'justify-right') { - leftOffset = lineDiff; - } - if (direction === 'rtl') { - leftOffset -= lineDiff; - } - return leftOffset; - }, - - /** - * @private - */ - _clearCache: function () { - this.__lineWidths = []; - this.__lineHeights = []; - this.__charBounds = []; - }, - - /** - * @private - */ - _shouldClearDimensionCache: function () { - var shouldClear = this._forceClearCache; - shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps')); - if (shouldClear) { - this.dirty = true; - this._forceClearCache = false; - } - return shouldClear; - }, - - /** - * Measure a single line given its index. Used to calculate the initial - * text bounding box. The values are calculated and stored in __lineWidths cache. - * @private - * @param {Number} lineIndex line number - * @return {Number} Line width - */ - getLineWidth: function (lineIndex) { - if (this.__lineWidths[lineIndex] !== undefined) { - return this.__lineWidths[lineIndex]; - } - - var lineInfo = this.measureLine(lineIndex); - var width = lineInfo.width; - this.__lineWidths[lineIndex] = width; - return width; - }, - - _getWidthOfCharSpacing: function () { - if (this.charSpacing !== 0) { - return this.fontSize * this.charSpacing / 1000; - } - return 0; - }, - - /** - * Retrieves the value of property at given character position - * @param {Number} lineIndex the line number - * @param {Number} charIndex the character number - * @param {String} property the property name - * @returns the value of 'property' - */ - getValueOfPropertyAt: function (lineIndex, charIndex, property) { - var charStyle = this._getStyleDeclaration(lineIndex, charIndex); - if (charStyle && typeof charStyle[property] !== 'undefined') { - return charStyle[property]; - } - return this[property]; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderTextDecoration: function (ctx, type) { - if (!this[type] && !this.styleHas(type)) { - return; - } - var heightOfLine, size, _size, - lineLeftOffset, dy, _dy, - line, lastDecoration, - leftOffset = this._getLeftOffset(), - topOffset = this._getTopOffset(), top, - boxStart, boxWidth, charBox, currentDecoration, - maxHeight, currentFill, lastFill, path = this.path, - charSpacing = this._getWidthOfCharSpacing(), - offsetY = this.offsets[type]; - - for (var i = 0, len = this._textLines.length; i < len; i++) { - heightOfLine = this.getHeightOfLine(i); - if (!this[type] && !this.styleHas(type, i)) { - topOffset += heightOfLine; - continue; - } - line = this._textLines[i]; - maxHeight = heightOfLine / this.lineHeight; - lineLeftOffset = this._getLineLeftOffset(i); - boxStart = 0; - boxWidth = 0; - lastDecoration = this.getValueOfPropertyAt(i, 0, type); - lastFill = this.getValueOfPropertyAt(i, 0, 'fill'); - top = topOffset + maxHeight * (1 - this._fontSizeFraction); - size = this.getHeightOfChar(i, 0); - dy = this.getValueOfPropertyAt(i, 0, 'deltaY'); - for (var j = 0, jlen = line.length; j < jlen; j++) { - charBox = this.__charBounds[i][j]; - currentDecoration = this.getValueOfPropertyAt(i, j, type); - currentFill = this.getValueOfPropertyAt(i, j, 'fill'); - _size = this.getHeightOfChar(i, j); - _dy = this.getValueOfPropertyAt(i, j, 'deltaY'); - if (path && currentDecoration && currentFill) { - ctx.save(); - ctx.fillStyle = lastFill; - ctx.translate(charBox.renderLeft, charBox.renderTop); - ctx.rotate(charBox.angle); - ctx.fillRect( - -charBox.kernedWidth / 2, - offsetY * _size + _dy, - charBox.kernedWidth, - this.fontSize / 15 - ); - ctx.restore(); - } - else if ( - (currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) - && boxWidth > 0 - ) { - var drawStart = leftOffset + lineLeftOffset + boxStart; - if (this.direction === 'rtl') { - drawStart = this.width - drawStart - boxWidth; - } - if (lastDecoration && lastFill) { - ctx.fillStyle = lastFill; - ctx.fillRect( - drawStart, - top + offsetY * size + dy, - boxWidth, - this.fontSize / 15 - ); - } - boxStart = charBox.left; - boxWidth = charBox.width; - lastDecoration = currentDecoration; - lastFill = currentFill; - size = _size; - dy = _dy; - } - else { - boxWidth += charBox.kernedWidth; - } - } - var drawStart = leftOffset + lineLeftOffset + boxStart; - if (this.direction === 'rtl') { - drawStart = this.width - drawStart - boxWidth; - } - ctx.fillStyle = currentFill; - currentDecoration && currentFill && ctx.fillRect( - drawStart, - top + offsetY * size + dy, - boxWidth - charSpacing, - this.fontSize / 15 - ); - topOffset += heightOfLine; - } - // if there is text background color no - // other shadows should be casted - this._removeShadow(ctx); - }, - - /** - * return font declaration string for canvas context - * @param {Object} [styleObject] object - * @returns {String} font declaration formatted for canvas context. - */ - _getFontDeclaration: function (styleObject, forMeasuring) { - var style = styleObject || this, family = this.fontFamily, - fontIsGeneric = fabric.Text.genericFonts.indexOf(family.toLowerCase()) > -1; - var fontFamily = family === undefined || - family.indexOf('\'') > -1 || family.indexOf(',') > -1 || - family.indexOf('"') > -1 || fontIsGeneric - ? style.fontFamily : '"' + style.fontFamily + '"'; - return [ - // node-canvas needs "weight style", while browsers need "style weight" - // verify if this can be fixed in JSDOM - (fabric.isLikelyNode ? style.fontWeight : style.fontStyle), - (fabric.isLikelyNode ? style.fontStyle : style.fontWeight), - forMeasuring ? this.CACHE_FONT_SIZE + 'px' : style.fontSize + 'px', - fontFamily - ].join(' '); - }, - - /** - * Renders text instance on a specified context - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - render: function (ctx) { - // do not render if object is not visible - if (!this.visible) { - return; - } - if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { - return; - } - if (this._shouldClearDimensionCache()) { - this.initDimensions(); - } - this.callSuper('render', ctx); - }, - - /** - * Returns the text as an array of lines. - * @param {String} text text to split - * @returns {Array} Lines in the text - */ - _splitTextIntoLines: function (text) { - var lines = text.split(this._reNewline), - newLines = new Array(lines.length), - newLine = ['\n'], - newText = []; - for (var i = 0; i < lines.length; i++) { - newLines[i] = fabric.util.string.graphemeSplit(lines[i]); - newText = newText.concat(newLines[i], newLine); - } - newText.pop(); - return { _unwrappedLines: newLines, lines: lines, graphemeText: newText, graphemeLines: newLines }; - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of an instance - */ - toObject: function (propertiesToInclude) { - var allProperties = additionalProps.concat(propertiesToInclude); - var obj = this.callSuper('toObject', allProperties); - obj.styles = fabric.util.stylesToArray(this.styles, this.text); - if (obj.path) { - obj.path = this.path.toObject(); - } - return obj; - }, - - /** - * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. - * @param {String|Object} key Property name or object (if object, iterate over the object properties) - * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) - * @return {fabric.Object} thisArg - * @chainable - */ - set: function (key, value) { - this.callSuper('set', key, value); - var needsDims = false; - var isAddingPath = false; - if (typeof key === 'object') { - for (var _key in key) { - if (_key === 'path') { - this.setPathInfo(); - } - needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1; - isAddingPath = isAddingPath || _key === 'path'; - } - } - else { - needsDims = this._dimensionAffectingProps.indexOf(key) !== -1; - isAddingPath = key === 'path'; - } - if (isAddingPath) { - this.setPathInfo(); - } - if (needsDims) { - this.initDimensions(); - this.setCoords(); - } - return this; - }, - - /** - * Returns complexity of an instance - * @return {Number} complexity - */ - complexity: function () { - return 1; - } - }); - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement}) - * @static - * @memberOf fabric.Text - * @see: http://www.w3.org/TR/SVG/text.html#TextElement - */ - fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( - 'x y dx dy font-family font-style font-weight font-size letter-spacing text-decoration text-anchor'.split(' ')); - - /** - * Default SVG font size - * @static - * @memberOf fabric.Text - */ - fabric.Text.DEFAULT_SVG_FONT_SIZE = 16; - - /** - * Returns fabric.Text instance from an SVG element (not yet implemented) - * @static - * @memberOf fabric.Text - * @param {SVGElement} element Element to parse - * @param {Function} callback callback function invoked after parsing - * @param {Object} [options] Options object - */ - fabric.Text.fromElement = function (element, callback, options) { - if (!element) { - return callback(null); - } - - var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES), - parsedAnchor = parsedAttributes.textAnchor || 'left'; - options = fabric.util.object.extend((options ? clone(options) : {}), parsedAttributes); - - options.top = options.top || 0; - options.left = options.left || 0; - if (parsedAttributes.textDecoration) { - var textDecoration = parsedAttributes.textDecoration; - if (textDecoration.indexOf('underline') !== -1) { - options.underline = true; - } - if (textDecoration.indexOf('overline') !== -1) { - options.overline = true; - } - if (textDecoration.indexOf('line-through') !== -1) { - options.linethrough = true; - } - delete options.textDecoration; - } - if ('dx' in parsedAttributes) { - options.left += parsedAttributes.dx; - } - if ('dy' in parsedAttributes) { - options.top += parsedAttributes.dy; - } - if (!('fontSize' in options)) { - options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; - } - - var textContent = ''; - - // The XML is not properly parsed in IE9 so a workaround to get - // textContent is through firstChild.data. Another workaround would be - // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does) - if (!('textContent' in element)) { - if ('firstChild' in element && element.firstChild !== null) { - if ('data' in element.firstChild && element.firstChild.data !== null) { - textContent = element.firstChild.data; - } - } - } - else { - textContent = element.textContent; - } - - textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' '); - var originalStrokeWidth = options.strokeWidth; - options.strokeWidth = 0; - - var text = new fabric.Text(textContent, options), - textHeightScaleFactor = text.getScaledHeight() / text.height, - lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height, - scaledDiff = lineHeightDiff * textHeightScaleFactor, - textHeight = text.getScaledHeight() + scaledDiff, - offX = 0; - /* - Adjust positioning: - x/y attributes in SVG correspond to the bottom-left corner of text bounding box - fabric output by default at top, left. - */ - if (parsedAnchor === 'center') { - offX = text.getScaledWidth() / 2; - } - if (parsedAnchor === 'right') { - offX = text.getScaledWidth(); - } - text.set({ - left: text.left - offX, - top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight, - strokeWidth: typeof originalStrokeWidth !== 'undefined' ? originalStrokeWidth : 1, - }); - callback(text); - }; - /* _FROM_SVG_END_ */ - - /** - * Returns fabric.Text instance from an object representation - * @static - * @memberOf fabric.Text - * @param {Object} object plain js Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created - */ - fabric.Text.fromObject = function (object, callback) { - var objectCopy = clone(object), path = object.path; - delete objectCopy.path; - return fabric.Object._fromObject('Text', objectCopy, function (textInstance) { - textInstance.styles = fabric.util.stylesFromArray(object.styles, object.text); - if (path) { - fabric.Object._fromObject('Path', path, function (pathInstance) { - textInstance.set('path', pathInstance); - callback(textInstance); - }, 'path'); - } - else { - callback(textInstance); - } - }, 'text'); - }; - - fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace']; - - fabric.util.createAccessors && fabric.util.createAccessors(fabric.Text); - -})(typeof exports !== 'undefined' ? exports : this); - - -(function () { - fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { - /** - * Returns true if object has no styling or no styling in a line - * @param {Number} lineIndex , lineIndex is on wrapped lines. - * @return {Boolean} - */ - isEmptyStyles: function (lineIndex) { - if (!this.styles) { - return true; - } - if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { - return true; - } - var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; - for (var p1 in obj) { - for (var p2 in obj[p1]) { - // eslint-disable-next-line no-unused-vars - for (var p3 in obj[p1][p2]) { - return false; - } - } - } - return true; - }, - - /** - * Returns true if object has a style property or has it ina specified line - * This function is used to detect if a text will use a particular property or not. - * @param {String} property to check for - * @param {Number} lineIndex to check the style on - * @return {Boolean} - */ - styleHas: function (property, lineIndex) { - if (!this.styles || !property || property === '') { - return false; - } - if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { - return false; - } - var obj = typeof lineIndex === 'undefined' ? this.styles : { 0: this.styles[lineIndex] }; - // eslint-disable-next-line - for (var p1 in obj) { - // eslint-disable-next-line - for (var p2 in obj[p1]) { - if (typeof obj[p1][p2][property] !== 'undefined') { - return true; - } - } - } - return false; - }, - - /** - * Check if characters in a text have a value for a property - * whose value matches the textbox's value for that property. If so, - * the character-level property is deleted. If the character - * has no other properties, then it is also deleted. Finally, - * if the line containing that character has no other characters - * then it also is deleted. - * - * @param {string} property The property to compare between characters and text. - */ - cleanStyle: function (property) { - if (!this.styles || !property || property === '') { - return false; - } - var obj = this.styles, stylesCount = 0, letterCount, stylePropertyValue, - allStyleObjectPropertiesMatch = true, graphemeCount = 0, styleObject; - // eslint-disable-next-line - for (var p1 in obj) { - letterCount = 0; - // eslint-disable-next-line - for (var p2 in obj[p1]) { - var styleObject = obj[p1][p2], - stylePropertyHasBeenSet = styleObject.hasOwnProperty(property); - - stylesCount++; - - if (stylePropertyHasBeenSet) { - if (!stylePropertyValue) { - stylePropertyValue = styleObject[property]; - } - else if (styleObject[property] !== stylePropertyValue) { - allStyleObjectPropertiesMatch = false; - } - - if (styleObject[property] === this[property]) { - delete styleObject[property]; - } - } - else { - allStyleObjectPropertiesMatch = false; - } - - if (Object.keys(styleObject).length !== 0) { - letterCount++; - } - else { - delete obj[p1][p2]; - } - } - - if (letterCount === 0) { - delete obj[p1]; - } - } - // if every grapheme has the same style set then - // delete those styles and set it on the parent - for (var i = 0; i < this._textLines.length; i++) { - graphemeCount += this._textLines[i].length; - } - if (allStyleObjectPropertiesMatch && stylesCount === graphemeCount) { - this[property] = stylePropertyValue; - this.removeStyle(property); - } - }, - - /** - * Remove a style property or properties from all individual character styles - * in a text object. Deletes the character style object if it contains no other style - * props. Deletes a line style object if it contains no other character styles. - * - * @param {String} props The property to remove from character styles. - */ - removeStyle: function (property) { - if (!this.styles || !property || property === '') { - return; - } - var obj = this.styles, line, lineNum, charNum; - for (lineNum in obj) { - line = obj[lineNum]; - for (charNum in line) { - delete line[charNum][property]; - if (Object.keys(line[charNum]).length === 0) { - delete line[charNum]; - } - } - if (Object.keys(line).length === 0) { - delete obj[lineNum]; - } - } - }, - - /** - * @private - */ - _extendStyles: function (index, styles) { - var loc = this.get2DCursorLocation(index); - - if (!this._getLineStyle(loc.lineIndex)) { - this._setLineStyle(loc.lineIndex); - } - - if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) { - this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {}); - } - - fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles); - }, - - /** - * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) - * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. - * @param {Boolean} [skipWrapping] consider the location for unwrapped lines. useful to manage styles. - */ - get2DCursorLocation: function (selectionStart, skipWrapping) { - if (typeof selectionStart === 'undefined') { - selectionStart = this.selectionStart; - } - var lines = skipWrapping ? this._unwrappedTextLines : this._textLines, - len = lines.length; - for (var i = 0; i < len; i++) { - if (selectionStart <= lines[i].length) { - return { - lineIndex: i, - charIndex: selectionStart - }; - } - selectionStart -= lines[i].length + this.missingNewlineOffset(i); - } - return { - lineIndex: i - 1, - charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart - }; - }, - - /** - * Gets style of a current selection/cursor (at the start position) - * if startIndex or endIndex are not provided, selectionStart or selectionEnd will be used. - * @param {Number} [startIndex] Start index to get styles at - * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 - * @param {Boolean} [complete] get full style or not - * @return {Array} styles an array with one, zero or more Style objects - */ - getSelectionStyles: function (startIndex, endIndex, complete) { - if (typeof startIndex === 'undefined') { - startIndex = this.selectionStart || 0; - } - if (typeof endIndex === 'undefined') { - endIndex = this.selectionEnd || startIndex; - } - var styles = []; - for (var i = startIndex; i < endIndex; i++) { - styles.push(this.getStyleAtPosition(i, complete)); - } - return styles; - }, - - /** - * Gets style of a current selection/cursor position - * @param {Number} position to get styles at - * @param {Boolean} [complete] full style if true - * @return {Object} style Style object at a specified index - * @private - */ - getStyleAtPosition: function (position, complete) { - var loc = this.get2DCursorLocation(position), - style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) : - this._getStyleDeclaration(loc.lineIndex, loc.charIndex); - return style || {}; - }, - - /** - * Sets style of a current selection, if no selection exist, do not set anything. - * @param {Object} [styles] Styles object - * @param {Number} [startIndex] Start index to get styles at - * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 - * @return {fabric.IText} thisArg - * @chainable - */ - setSelectionStyles: function (styles, startIndex, endIndex) { - if (typeof startIndex === 'undefined') { - startIndex = this.selectionStart || 0; - } - if (typeof endIndex === 'undefined') { - endIndex = this.selectionEnd || startIndex; - } - for (var i = startIndex; i < endIndex; i++) { - this._extendStyles(i, styles); - } - /* not included in _extendStyles to avoid clearing cache more than once */ - this._forceClearCache = true; - return this; - }, - - /** - * get the reference, not a clone, of the style object for a given character - * @param {Number} lineIndex - * @param {Number} charIndex - * @return {Object} style object - */ - _getStyleDeclaration: function (lineIndex, charIndex) { - var lineStyle = this.styles && this.styles[lineIndex]; - if (!lineStyle) { - return null; - } - return lineStyle[charIndex]; - }, - - /** - * return a new object that contains all the style property for a character - * the object returned is newly created - * @param {Number} lineIndex of the line where the character is - * @param {Number} charIndex position of the character on the line - * @return {Object} style object - */ - getCompleteStyleDeclaration: function (lineIndex, charIndex) { - var style = this._getStyleDeclaration(lineIndex, charIndex) || {}, - styleObject = {}, prop; - for (var i = 0; i < this._styleProperties.length; i++) { - prop = this._styleProperties[i]; - styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop]; - } - return styleObject; - }, - - /** - * @param {Number} lineIndex - * @param {Number} charIndex - * @param {Object} style - * @private - */ - _setStyleDeclaration: function (lineIndex, charIndex, style) { - this.styles[lineIndex][charIndex] = style; - }, - - /** - * - * @param {Number} lineIndex - * @param {Number} charIndex - * @private - */ - _deleteStyleDeclaration: function (lineIndex, charIndex) { - delete this.styles[lineIndex][charIndex]; - }, - - /** - * @param {Number} lineIndex - * @return {Boolean} if the line exists or not - * @private - */ - _getLineStyle: function (lineIndex) { - return !!this.styles[lineIndex]; - }, - - /** - * Set the line style to an empty object so that is initialized - * @param {Number} lineIndex - * @private - */ - _setLineStyle: function (lineIndex) { - this.styles[lineIndex] = {}; - }, - - /** - * @param {Number} lineIndex - * @private - */ - _deleteLineStyle: function (lineIndex) { - delete this.styles[lineIndex]; - } - }); -})(); - - -(function () { - - function parseDecoration(object) { - if (object.textDecoration) { - object.textDecoration.indexOf('underline') > -1 && (object.underline = true); - object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true); - object.textDecoration.indexOf('overline') > -1 && (object.overline = true); - delete object.textDecoration; - } - } - - /** - * IText class (introduced in v1.4) Events are also fired with "text:" - * prefix when observing canvas. - * @class fabric.IText - * @extends fabric.Text - * @mixes fabric.Observable - * - * @fires changed - * @fires selection:changed - * @fires editing:entered - * @fires editing:exited - * - * @return {fabric.IText} thisArg - * @see {@link fabric.IText#initialize} for constructor definition - * - *

Supported key combinations:

- *
-     *   Move cursor:                    left, right, up, down
-     *   Select character:               shift + left, shift + right
-     *   Select text vertically:         shift + up, shift + down
-     *   Move cursor by word:            alt + left, alt + right
-     *   Select words:                   shift + alt + left, shift + alt + right
-     *   Move cursor to line start/end:  cmd + left, cmd + right or home, end
-     *   Select till start/end of line:  cmd + shift + left, cmd + shift + right or shift + home, shift + end
-     *   Jump to start/end of text:      cmd + up, cmd + down
-     *   Select till start/end of text:  cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
-     *   Delete character:               backspace
-     *   Delete word:                    alt + backspace
-     *   Delete line:                    cmd + backspace
-     *   Forward delete:                 delete
-     *   Copy text:                      ctrl/cmd + c
-     *   Paste text:                     ctrl/cmd + v
-     *   Cut text:                       ctrl/cmd + x
-     *   Select entire text:             ctrl/cmd + a
-     *   Quit editing                    tab or esc
-     * 
- * - *

Supported mouse/touch combination

- *
-     *   Position cursor:                click/touch
-     *   Create selection:               click/touch & drag
-     *   Create selection:               click & shift + click
-     *   Select word:                    double click
-     *   Select line:                    triple click
-     * 
- */ - fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'i-text', - - /** - * Index where text selection starts (or where cursor is when there is no selection) - * @type Number - * @default - */ - selectionStart: 0, - - /** - * Index where text selection ends - * @type Number - * @default - */ - selectionEnd: 0, - - /** - * Color of text selection - * @type String - * @default - */ - selectionColor: 'rgba(17,119,255,0.3)', - - /** - * Indicates whether text is in editing mode - * @type Boolean - * @default - */ - isEditing: false, - - /** - * Indicates whether a text can be edited - * @type Boolean - * @default - */ - editable: true, - - /** - * Border color of text object while it's in editing mode - * @type String - * @default - */ - editingBorderColor: 'rgba(102,153,255,0.25)', - - /** - * Width of cursor (in px) - * @type Number - * @default - */ - cursorWidth: 2, - - /** - * Color of text cursor color in editing mode. - * if not set (default) will take color from the text. - * if set to a color value that fabric can understand, it will - * be used instead of the color of the text at the current position. - * @type String - * @default - */ - cursorColor: '', - - /** - * Delay between cursor blink (in ms) - * @type Number - * @default - */ - cursorDelay: 1000, - - /** - * Duration of cursor fadein (in ms) - * @type Number - * @default - */ - cursorDuration: 600, - - /** - * Indicates whether internal text char widths can be cached - * @type Boolean - * @default - */ - caching: true, - - /** - * DOM container to append the hiddenTextarea. - * An alternative to attaching to the document.body. - * Useful to reduce laggish redraw of the full document.body tree and - * also with modals event capturing that won't let the textarea take focus. - * @type HTMLElement - * @default - */ - hiddenTextareaContainer: null, - - /** - * @private - */ - _reSpace: /\s|\n/, - - /** - * @private - */ - _currentCursorOpacity: 0, - - /** - * @private - */ - _selectionDirection: null, - - /** - * @private - */ - _abortCursorAnimation: false, - - /** - * @private - */ - __widthOfSpace: [], - - /** - * Helps determining when the text is in composition, so that the cursor - * rendering is altered. - */ - inCompositionMode: false, - - /** - * Constructor - * @param {String} text Text string - * @param {Object} [options] Options object - * @return {fabric.IText} thisArg - */ - initialize: function (text, options) { - this.callSuper('initialize', text, options); - this.initBehavior(); - }, - - /** - * Sets selection start (left boundary of a selection) - * @param {Number} index Index to set selection start to - */ - setSelectionStart: function (index) { - index = Math.max(index, 0); - this._updateAndFire('selectionStart', index); - }, - - /** - * Sets selection end (right boundary of a selection) - * @param {Number} index Index to set selection end to - */ - setSelectionEnd: function (index) { - index = Math.min(index, this.text.length); - this._updateAndFire('selectionEnd', index); - }, - - /** - * @private - * @param {String} property 'selectionStart' or 'selectionEnd' - * @param {Number} index new position of property - */ - _updateAndFire: function (property, index) { - if (this[property] !== index) { - this._fireSelectionChanged(); - this[property] = index; - } - this._updateTextarea(); - }, - - /** - * Fires the even of selection changed - * @private - */ - _fireSelectionChanged: function () { - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); - }, - - /** - * Initialize text dimensions. Render all text on given context - * or on a offscreen canvas to get the text width with measureText. - * Updates this.width and this.height with the proper values. - * Does not return dimensions. - * @private - */ - initDimensions: function () { - this.isEditing && this.initDelayedCursor(); - this.clearContextTop(); - this.callSuper('initDimensions'); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - render: function (ctx) { - this.clearContextTop(); - this.callSuper('render', ctx); - // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor - // the correct position but not at every cursor animation. - this.cursorOffsetCache = {}; - this.renderCursorOrSelection(); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - this.callSuper('_render', ctx); - }, - - /** - * Prepare and clean the contextTop - */ - clearContextTop: function (skipRestore) { - if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { - return; - } - var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform; - ctx.save(); - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - this.transform(ctx); - this._clearTextArea(ctx); - skipRestore || ctx.restore(); - }, - /** - * Renders cursor or selection (depending on what exists) - * it does on the contextTop. If contextTop is not available, do nothing. - */ - renderCursorOrSelection: function () { - if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { - return; - } - var boundaries = this._getCursorBoundaries(), - ctx = this.canvas.contextTop; - this.clearContextTop(true); - if (this.selectionStart === this.selectionEnd) { - this.renderCursor(boundaries, ctx); - } - else { - this.renderSelection(boundaries, ctx); - } - ctx.restore(); - }, - - _clearTextArea: function (ctx) { - // we add 4 pixel, to be sure to do not leave any pixel out - var width = this.width + 4, height = this.height + 4; - ctx.clearRect(-width / 2, -height / 2, width, height); - }, - - /** - * Returns cursor boundaries (left, top, leftOffset, topOffset) - * @private - * @param {Array} chars Array of characters - * @param {String} typeOfBoundaries - */ - _getCursorBoundaries: function (position) { - - // left/top are left/top of entire text box - // leftOffset/topOffset are offset from that left/top point of a text box - - if (typeof position === 'undefined') { - position = this.selectionStart; - } - - var left = this._getLeftOffset(), - top = this._getTopOffset(), - offsets = this._getCursorBoundariesOffsets(position); - return { - left: left, - top: top, - leftOffset: offsets.left, - topOffset: offsets.top - }; - }, - - /** - * @private - */ - _getCursorBoundariesOffsets: function (position) { - if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) { - return this.cursorOffsetCache; - } - var lineLeftOffset, - lineIndex, - charIndex, - topOffset = 0, - leftOffset = 0, - boundaries, - cursorPosition = this.get2DCursorLocation(position); - charIndex = cursorPosition.charIndex; - lineIndex = cursorPosition.lineIndex; - for (var i = 0; i < lineIndex; i++) { - topOffset += this.getHeightOfLine(i); - } - lineLeftOffset = this._getLineLeftOffset(lineIndex); - var bound = this.__charBounds[lineIndex][charIndex]; - bound && (leftOffset = bound.left); - if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) { - leftOffset -= this._getWidthOfCharSpacing(); - } - boundaries = { - top: topOffset, - left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), - }; - if (this.direction === 'rtl') { - boundaries.left *= -1; - } - this.cursorOffsetCache = boundaries; - return this.cursorOffsetCache; - }, - - /** - * Renders cursor - * @param {Object} boundaries - * @param {CanvasRenderingContext2D} ctx transformed context to draw on - */ - renderCursor: function (boundaries, ctx) { - var cursorLocation = this.get2DCursorLocation(), - lineIndex = cursorLocation.lineIndex, - charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0, - charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'), - multiplier = this.scaleX * this.canvas.getZoom(), - cursorWidth = this.cursorWidth / multiplier, - topOffset = boundaries.topOffset, - dy = this.getValueOfPropertyAt(lineIndex, charIndex, 'deltaY'); - topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight - - charHeight * (1 - this._fontSizeFraction); - - if (this.inCompositionMode) { - this.renderSelection(boundaries, ctx); - } - ctx.fillStyle = this.cursorColor || this.getValueOfPropertyAt(lineIndex, charIndex, 'fill'); - ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; - ctx.fillRect( - boundaries.left + boundaries.leftOffset - cursorWidth / 2, - topOffset + boundaries.top + dy, - cursorWidth, - charHeight); - }, - - /** - * Renders text selection - * @param {Object} boundaries Object with left/top/leftOffset/topOffset - * @param {CanvasRenderingContext2D} ctx transformed context to draw on - */ - renderSelection: function (boundaries, ctx) { - - var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart, - selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd, - isJustify = this.textAlign.indexOf('justify') !== -1, - start = this.get2DCursorLocation(selectionStart), - end = this.get2DCursorLocation(selectionEnd), - startLine = start.lineIndex, - endLine = end.lineIndex, - startChar = start.charIndex < 0 ? 0 : start.charIndex, - endChar = end.charIndex < 0 ? 0 : end.charIndex; - - for (var i = startLine; i <= endLine; i++) { - var lineOffset = this._getLineLeftOffset(i) || 0, - lineHeight = this.getHeightOfLine(i), - realLineHeight = 0, boxStart = 0, boxEnd = 0; - - if (i === startLine) { - boxStart = this.__charBounds[startLine][startChar].left; - } - if (i >= startLine && i < endLine) { - boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5; // WTF is this 5? - } - else if (i === endLine) { - if (endChar === 0) { - boxEnd = this.__charBounds[endLine][endChar].left; - } - else { - var charSpacing = this._getWidthOfCharSpacing(); - boxEnd = this.__charBounds[endLine][endChar - 1].left - + this.__charBounds[endLine][endChar - 1].width - charSpacing; - } - } - realLineHeight = lineHeight; - if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) { - lineHeight /= this.lineHeight; - } - var drawStart = boundaries.left + lineOffset + boxStart, - drawWidth = boxEnd - boxStart, - drawHeight = lineHeight, extraTop = 0; - if (this.inCompositionMode) { - ctx.fillStyle = this.compositionColor || 'black'; - drawHeight = 1; - extraTop = lineHeight; - } - else { - ctx.fillStyle = this.selectionColor; - } - if (this.direction === 'rtl') { - drawStart = this.width - drawStart - drawWidth; - } - ctx.fillRect( - drawStart, - boundaries.top + boundaries.topOffset + extraTop, - drawWidth, - drawHeight); - boundaries.topOffset += realLineHeight; - } - }, - - /** - * High level function to know the height of the cursor. - * the currentChar is the one that precedes the cursor - * Returns fontSize of char at the current cursor - * Unused from the library, is for the end user - * @return {Number} Character font size - */ - getCurrentCharFontSize: function () { - var cp = this._getCurrentCharIndex(); - return this.getValueOfPropertyAt(cp.l, cp.c, 'fontSize'); - }, - - /** - * High level function to know the color of the cursor. - * the currentChar is the one that precedes the cursor - * Returns color (fill) of char at the current cursor - * if the text object has a pattern or gradient for filler, it will return that. - * Unused by the library, is for the end user - * @return {String | fabric.Gradient | fabric.Pattern} Character color (fill) - */ - getCurrentCharColor: function () { - var cp = this._getCurrentCharIndex(); - return this.getValueOfPropertyAt(cp.l, cp.c, 'fill'); - }, - - /** - * Returns the cursor position for the getCurrent.. functions - * @private - */ - _getCurrentCharIndex: function () { - var cursorPosition = this.get2DCursorLocation(this.selectionStart, true), - charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0; - return { l: cursorPosition.lineIndex, c: charIndex }; - } - }); - - /** - * Returns fabric.IText instance from an object representation - * @static - * @memberOf fabric.IText - * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as argument - */ - fabric.IText.fromObject = function (object, callback) { - var styles = fabric.util.stylesFromArray(object.styles, object.text); - //copy object to prevent mutation - var objCopy = Object.assign({}, object, { styles: styles }); - parseDecoration(objCopy); - if (objCopy.styles) { - for (var i in objCopy.styles) { - for (var j in objCopy.styles[i]) { - parseDecoration(objCopy.styles[i][j]); - } - } - } - fabric.Object._fromObject('IText', objCopy, callback, 'text'); - }; -})(); - - -(function () { - - var clone = fabric.util.object.clone; - - fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { - - /** - * Initializes all the interactive behavior of IText - */ - initBehavior: function () { - this.initAddedHandler(); - this.initRemovedHandler(); - this.initCursorSelectionHandlers(); - this.initDoubleClickSimulation(); - this.mouseMoveHandler = this.mouseMoveHandler.bind(this); - }, - - onDeselect: function () { - this.isEditing && this.exitEditing(); - this.selected = false; - }, - - /** - * Initializes "added" event handler - */ - initAddedHandler: function () { - var _this = this; - this.on('added', function () { - var canvas = _this.canvas; - if (canvas) { - if (!canvas._hasITextHandlers) { - canvas._hasITextHandlers = true; - _this._initCanvasHandlers(canvas); - } - canvas._iTextInstances = canvas._iTextInstances || []; - canvas._iTextInstances.push(_this); - } - }); - }, - - initRemovedHandler: function () { - var _this = this; - this.on('removed', function () { - var canvas = _this.canvas; - if (canvas) { - canvas._iTextInstances = canvas._iTextInstances || []; - fabric.util.removeFromArray(canvas._iTextInstances, _this); - if (canvas._iTextInstances.length === 0) { - canvas._hasITextHandlers = false; - _this._removeCanvasHandlers(canvas); - } - } - }); - }, - - /** - * register canvas event to manage exiting on other instances - * @private - */ - _initCanvasHandlers: function (canvas) { - canvas._mouseUpITextHandler = function () { - if (canvas._iTextInstances) { - canvas._iTextInstances.forEach(function (obj) { - obj.__isMousedown = false; - }); - } - }; - canvas.on('mouse:up', canvas._mouseUpITextHandler); - }, - - /** - * remove canvas event to manage exiting on other instances - * @private - */ - _removeCanvasHandlers: function (canvas) { - canvas.off('mouse:up', canvas._mouseUpITextHandler); - }, - - /** - * @private - */ - _tick: function () { - this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete'); - }, - - /** - * @private - */ - _animateCursor: function (obj, targetOpacity, duration, completeMethod) { - - var tickState; - - tickState = { - isAborted: false, - abort: function () { - this.isAborted = true; - }, - }; - - obj.animate('_currentCursorOpacity', targetOpacity, { - duration: duration, - onComplete: function () { - if (!tickState.isAborted) { - obj[completeMethod](); - } - }, - onChange: function () { - // we do not want to animate a selection, only cursor - if (obj.canvas && obj.selectionStart === obj.selectionEnd) { - obj.renderCursorOrSelection(); - } - }, - abort: function () { - return tickState.isAborted; - } - }); - return tickState; - }, - - /** - * @private - */ - _onTickComplete: function () { - - var _this = this; - - if (this._cursorTimeout1) { - clearTimeout(this._cursorTimeout1); - } - this._cursorTimeout1 = setTimeout(function () { - _this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick'); - }, 100); - }, - - /** - * Initializes delayed cursor - */ - initDelayedCursor: function (restart) { - var _this = this, - delay = restart ? 0 : this.cursorDelay; - - this.abortCursorAnimation(); - this._currentCursorOpacity = 1; - this._cursorTimeout2 = setTimeout(function () { - _this._tick(); - }, delay); - }, - - /** - * Aborts cursor animation and clears all timeouts - */ - abortCursorAnimation: function () { - var shouldClear = this._currentTickState || this._currentTickCompleteState, - canvas = this.canvas; - this._currentTickState && this._currentTickState.abort(); - this._currentTickCompleteState && this._currentTickCompleteState.abort(); - - clearTimeout(this._cursorTimeout1); - clearTimeout(this._cursorTimeout2); - - this._currentCursorOpacity = 0; - // to clear just itext area we need to transform the context - // it may not be worth it - if (shouldClear && canvas) { - canvas.clearContext(canvas.contextTop || canvas.contextContainer); - } - - }, - - /** - * Selects entire text - * @return {fabric.IText} thisArg - * @chainable - */ - selectAll: function () { - this.selectionStart = 0; - this.selectionEnd = this._text.length; - this._fireSelectionChanged(); - this._updateTextarea(); - return this; - }, - - /** - * Returns selected text - * @return {String} - */ - getSelectedText: function () { - return this._text.slice(this.selectionStart, this.selectionEnd).join(''); - }, - - /** - * Find new selection index representing start of current word according to current selection index - * @param {Number} startFrom Current selection index - * @return {Number} New selection index - */ - findWordBoundaryLeft: function (startFrom) { - var offset = 0, index = startFrom - 1; - - // remove space before cursor first - if (this._reSpace.test(this._text[index])) { - while (this._reSpace.test(this._text[index])) { - offset++; - index--; - } - } - while (/\S/.test(this._text[index]) && index > -1) { - offset++; - index--; - } - - return startFrom - offset; - }, - - /** - * Find new selection index representing end of current word according to current selection index - * @param {Number} startFrom Current selection index - * @return {Number} New selection index - */ - findWordBoundaryRight: function (startFrom) { - var offset = 0, index = startFrom; - - // remove space after cursor first - if (this._reSpace.test(this._text[index])) { - while (this._reSpace.test(this._text[index])) { - offset++; - index++; - } - } - while (/\S/.test(this._text[index]) && index < this._text.length) { - offset++; - index++; - } - - return startFrom + offset; - }, - - /** - * Find new selection index representing start of current line according to current selection index - * @param {Number} startFrom Current selection index - * @return {Number} New selection index - */ - findLineBoundaryLeft: function (startFrom) { - var offset = 0, index = startFrom - 1; - - while (!/\n/.test(this._text[index]) && index > -1) { - offset++; - index--; - } - - return startFrom - offset; - }, - - /** - * Find new selection index representing end of current line according to current selection index - * @param {Number} startFrom Current selection index - * @return {Number} New selection index - */ - findLineBoundaryRight: function (startFrom) { - var offset = 0, index = startFrom; - - while (!/\n/.test(this._text[index]) && index < this._text.length) { - offset++; - index++; - } - - return startFrom + offset; - }, - - /** - * Finds index corresponding to beginning or end of a word - * @param {Number} selectionStart Index of a character - * @param {Number} direction 1 or -1 - * @return {Number} Index of the beginning or end of a word - */ - searchWordBoundary: function (selectionStart, direction) { - var text = this._text, - index = this._reSpace.test(text[selectionStart]) ? selectionStart - 1 : selectionStart, - _char = text[index], - // wrong - reNonWord = fabric.reNonWord; - - while (!reNonWord.test(_char) && index > 0 && index < text.length) { - index += direction; - _char = text[index]; - } - if (reNonWord.test(_char)) { - index += direction === 1 ? 0 : 1; - } - return index; - }, - - /** - * Selects a word based on the index - * @param {Number} selectionStart Index of a character - */ - selectWord: function (selectionStart) { - selectionStart = selectionStart || this.selectionStart; - var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */ - newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ - - this.selectionStart = newSelectionStart; - this.selectionEnd = newSelectionEnd; - this._fireSelectionChanged(); - this._updateTextarea(); - this.renderCursorOrSelection(); - }, - - /** - * Selects a line based on the index - * @param {Number} selectionStart Index of a character - * @return {fabric.IText} thisArg - * @chainable - */ - selectLine: function (selectionStart) { - selectionStart = selectionStart || this.selectionStart; - var newSelectionStart = this.findLineBoundaryLeft(selectionStart), - newSelectionEnd = this.findLineBoundaryRight(selectionStart); - - this.selectionStart = newSelectionStart; - this.selectionEnd = newSelectionEnd; - this._fireSelectionChanged(); - this._updateTextarea(); - return this; - }, - - /** - * Enters editing state - * @return {fabric.IText} thisArg - * @chainable - */ - enterEditing: function (e) { - if (this.isEditing || !this.editable) { - return; - } - - if (this.canvas) { - this.canvas.calcOffset(); - this.exitEditingOnOthers(this.canvas); - } - - this.isEditing = true; - - this.initHiddenTextarea(e); - this.hiddenTextarea.focus(); - this.hiddenTextarea.value = this.text; - this._updateTextarea(); - this._saveEditingProps(); - this._setEditingProps(); - this._textBeforeEdit = this.text; - - this._tick(); - this.fire('editing:entered'); - this._fireSelectionChanged(); - if (!this.canvas) { - return this; - } - this.canvas.fire('text:editing:entered', { target: this }); - this.initMouseMoveHandler(); - this.canvas.requestRenderAll(); - return this; - }, - - exitEditingOnOthers: function (canvas) { - if (canvas._iTextInstances) { - canvas._iTextInstances.forEach(function (obj) { - obj.selected = false; - if (obj.isEditing) { - obj.exitEditing(); - } - }); - } - }, - - /** - * Initializes "mousemove" event handler - */ - initMouseMoveHandler: function () { - this.canvas.on('mouse:move', this.mouseMoveHandler); - }, - - /** - * @private - */ - mouseMoveHandler: function (options) { - if (!this.__isMousedown || !this.isEditing) { - return; - } - - // regain focus - document.activeElement !== this.hiddenTextarea && this.hiddenTextarea.focus(); - - var newSelectionStart = this.getSelectionStartFromPointer(options.e), - currentStart = this.selectionStart, - currentEnd = this.selectionEnd; - if ( - (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd) - && - (currentStart === newSelectionStart || currentEnd === newSelectionStart) - ) { - return; - } - if (newSelectionStart > this.__selectionStartOnMouseDown) { - this.selectionStart = this.__selectionStartOnMouseDown; - this.selectionEnd = newSelectionStart; - } - else { - this.selectionStart = newSelectionStart; - this.selectionEnd = this.__selectionStartOnMouseDown; - } - if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) { - this.restartCursorIfNeeded(); - this._fireSelectionChanged(); - this._updateTextarea(); - this.renderCursorOrSelection(); - } - }, - - /** - * @private - */ - _setEditingProps: function () { - this.hoverCursor = 'text'; - - if (this.canvas) { - this.canvas.defaultCursor = this.canvas.moveCursor = 'text'; - } - - this.borderColor = this.editingBorderColor; - this.hasControls = this.selectable = false; - this.lockMovementX = this.lockMovementY = true; - }, - - /** - * convert from textarea to grapheme indexes - */ - fromStringToGraphemeSelection: function (start, end, text) { - var smallerTextStart = text.slice(0, start), - graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length; - if (start === end) { - return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; - } - var smallerTextEnd = text.slice(start, end), - graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length; - return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; - }, - - /** - * convert from fabric to textarea values - */ - fromGraphemeToStringSelection: function (start, end, _text) { - var smallerTextStart = _text.slice(0, start), - graphemeStart = smallerTextStart.join('').length; - if (start === end) { - return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; - } - var smallerTextEnd = _text.slice(start, end), - graphemeEnd = smallerTextEnd.join('').length; - return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; - }, - - /** - * @private - */ - _updateTextarea: function () { - this.cursorOffsetCache = {}; - if (!this.hiddenTextarea) { - return; - } - if (!this.inCompositionMode) { - var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text); - this.hiddenTextarea.selectionStart = newSelection.selectionStart; - this.hiddenTextarea.selectionEnd = newSelection.selectionEnd; - } - this.updateTextareaPosition(); - }, - - /** - * @private - */ - updateFromTextArea: function () { - if (!this.hiddenTextarea) { - return; - } - this.cursorOffsetCache = {}; - this.text = this.hiddenTextarea.value; - if (this._shouldClearDimensionCache()) { - this.initDimensions(); - this.setCoords(); - } - var newSelection = this.fromStringToGraphemeSelection( - this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value); - this.selectionEnd = this.selectionStart = newSelection.selectionEnd; - if (!this.inCompositionMode) { - this.selectionStart = newSelection.selectionStart; - } - this.updateTextareaPosition(); - }, - - /** - * @private - */ - updateTextareaPosition: function () { - if (this.selectionStart === this.selectionEnd) { - var style = this._calcTextareaPosition(); - this.hiddenTextarea.style.left = style.left; - this.hiddenTextarea.style.top = style.top; - } - }, - - /** - * @private - * @return {Object} style contains style for hiddenTextarea - */ - _calcTextareaPosition: function () { - if (!this.canvas) { - return { x: 1, y: 1 }; - } - var desiredPosition = this.inCompositionMode ? this.compositionStart : this.selectionStart, - boundaries = this._getCursorBoundaries(desiredPosition), - cursorLocation = this.get2DCursorLocation(desiredPosition), - lineIndex = cursorLocation.lineIndex, - charIndex = cursorLocation.charIndex, - charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight, - leftOffset = boundaries.leftOffset, - m = this.calcTransformMatrix(), - p = { - x: boundaries.left + leftOffset, - y: boundaries.top + boundaries.topOffset + charHeight - }, - retinaScaling = this.canvas.getRetinaScaling(), - upperCanvas = this.canvas.upperCanvasEl, - upperCanvasWidth = upperCanvas.width / retinaScaling, - upperCanvasHeight = upperCanvas.height / retinaScaling, - maxWidth = upperCanvasWidth - charHeight, - maxHeight = upperCanvasHeight - charHeight, - scaleX = upperCanvas.clientWidth / upperCanvasWidth, - scaleY = upperCanvas.clientHeight / upperCanvasHeight; - - p = fabric.util.transformPoint(p, m); - p = fabric.util.transformPoint(p, this.canvas.viewportTransform); - p.x *= scaleX; - p.y *= scaleY; - if (p.x < 0) { - p.x = 0; - } - if (p.x > maxWidth) { - p.x = maxWidth; - } - if (p.y < 0) { - p.y = 0; - } - if (p.y > maxHeight) { - p.y = maxHeight; - } - - // add canvas offset on document - p.x += this.canvas._offset.left; - p.y += this.canvas._offset.top; - - return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight }; - }, - - /** - * @private - */ - _saveEditingProps: function () { - this._savedProps = { - hasControls: this.hasControls, - borderColor: this.borderColor, - lockMovementX: this.lockMovementX, - lockMovementY: this.lockMovementY, - hoverCursor: this.hoverCursor, - selectable: this.selectable, - defaultCursor: this.canvas && this.canvas.defaultCursor, - moveCursor: this.canvas && this.canvas.moveCursor - }; - }, - - /** - * @private - */ - _restoreEditingProps: function () { - if (!this._savedProps) { - return; - } - - this.hoverCursor = this._savedProps.hoverCursor; - this.hasControls = this._savedProps.hasControls; - this.borderColor = this._savedProps.borderColor; - this.selectable = this._savedProps.selectable; - this.lockMovementX = this._savedProps.lockMovementX; - this.lockMovementY = this._savedProps.lockMovementY; - - if (this.canvas) { - this.canvas.defaultCursor = this._savedProps.defaultCursor; - this.canvas.moveCursor = this._savedProps.moveCursor; - } - }, - - /** - * Exits from editing state - * @return {fabric.IText} thisArg - * @chainable - */ - exitEditing: function () { - var isTextChanged = (this._textBeforeEdit !== this.text); - var hiddenTextarea = this.hiddenTextarea; - this.selected = false; - this.isEditing = false; - - this.selectionEnd = this.selectionStart; - - if (hiddenTextarea) { - hiddenTextarea.blur && hiddenTextarea.blur(); - hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea); - } - this.hiddenTextarea = null; - this.abortCursorAnimation(); - this._restoreEditingProps(); - this._currentCursorOpacity = 0; - if (this._shouldClearDimensionCache()) { - this.initDimensions(); - this.setCoords(); - } - this.fire('editing:exited'); - isTextChanged && this.fire('modified'); - if (this.canvas) { - this.canvas.off('mouse:move', this.mouseMoveHandler); - this.canvas.fire('text:editing:exited', { target: this }); - isTextChanged && this.canvas.fire('object:modified', { target: this }); - } - return this; - }, - - /** - * @private - */ - _removeExtraneousStyles: function () { - for (var prop in this.styles) { - if (!this._textLines[prop]) { - delete this.styles[prop]; - } - } - }, - - /** - * remove and reflow a style block from start to end. - * @param {Number} start linear start position for removal (included in removal) - * @param {Number} end linear end position for removal ( excluded from removal ) - */ - removeStyleFromTo: function (start, end) { - var cursorStart = this.get2DCursorLocation(start, true), - cursorEnd = this.get2DCursorLocation(end, true), - lineStart = cursorStart.lineIndex, - charStart = cursorStart.charIndex, - lineEnd = cursorEnd.lineIndex, - charEnd = cursorEnd.charIndex, - i, styleObj; - if (lineStart !== lineEnd) { - // step1 remove the trailing of lineStart - if (this.styles[lineStart]) { - for (i = charStart; i < this._unwrappedTextLines[lineStart].length; i++) { - delete this.styles[lineStart][i]; - } - } - // step2 move the trailing of lineEnd to lineStart if needed - if (this.styles[lineEnd]) { - for (i = charEnd; i < this._unwrappedTextLines[lineEnd].length; i++) { - styleObj = this.styles[lineEnd][i]; - if (styleObj) { - this.styles[lineStart] || (this.styles[lineStart] = {}); - this.styles[lineStart][charStart + i - charEnd] = styleObj; - } - } - } - // step3 detects lines will be completely removed. - for (i = lineStart + 1; i <= lineEnd; i++) { - delete this.styles[i]; - } - // step4 shift remaining lines. - this.shiftLineStyles(lineEnd, lineStart - lineEnd); - } - else { - // remove and shift left on the same line - if (this.styles[lineStart]) { - styleObj = this.styles[lineStart]; - var diff = charEnd - charStart, numericChar, _char; - for (i = charStart; i < charEnd; i++) { - delete styleObj[i]; - } - for (_char in this.styles[lineStart]) { - numericChar = parseInt(_char, 10); - if (numericChar >= charEnd) { - styleObj[numericChar - diff] = styleObj[_char]; - delete styleObj[_char]; - } - } - } - } - }, - - /** - * Shifts line styles up or down - * @param {Number} lineIndex Index of a line - * @param {Number} offset Can any number? - */ - shiftLineStyles: function (lineIndex, offset) { - // shift all line styles by offset upward or downward - // do not clone deep. we need new array, not new style objects - var clonedStyles = clone(this.styles); - for (var line in this.styles) { - var numericLine = parseInt(line, 10); - if (numericLine > lineIndex) { - this.styles[numericLine + offset] = clonedStyles[numericLine]; - if (!clonedStyles[numericLine - offset]) { - delete this.styles[numericLine]; - } - } - } - }, - - restartCursorIfNeeded: function () { - if (!this._currentTickState || this._currentTickState.isAborted - || !this._currentTickCompleteState || this._currentTickCompleteState.isAborted - ) { - this.initDelayedCursor(); - } - }, - - /** - * Handle insertion of more consecutive style lines for when one or more - * newlines gets added to the text. Since current style needs to be shifted - * first we shift the current style of the number lines needed, then we add - * new lines from the last to the first. - * @param {Number} lineIndex Index of a line - * @param {Number} charIndex Index of a char - * @param {Number} qty number of lines to add - * @param {Array} copiedStyle Array of objects styles - */ - insertNewlineStyleObject: function (lineIndex, charIndex, qty, copiedStyle) { - var currentCharStyle, - newLineStyles = {}, - somethingAdded = false, - isEndOfLine = this._unwrappedTextLines[lineIndex].length === charIndex; - - qty || (qty = 1); - this.shiftLineStyles(lineIndex, qty); - if (this.styles[lineIndex]) { - currentCharStyle = this.styles[lineIndex][charIndex === 0 ? charIndex : charIndex - 1]; - } - // we clone styles of all chars - // after cursor onto the current line - for (var index in this.styles[lineIndex]) { - var numIndex = parseInt(index, 10); - if (numIndex >= charIndex) { - somethingAdded = true; - newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index]; - // remove lines from the previous line since they're on a new line now - if (!(isEndOfLine && charIndex === 0)) { - delete this.styles[lineIndex][index]; - } - } - } - var styleCarriedOver = false; - if (somethingAdded && !isEndOfLine) { - // if is end of line, the extra style we copied - // is probably not something we want - this.styles[lineIndex + qty] = newLineStyles; - styleCarriedOver = true; - } - if (styleCarriedOver) { - // skip the last line of since we already prepared it. - qty--; - } - // for the all the lines or all the other lines - // we clone current char style onto the next (otherwise empty) line - while (qty > 0) { - if (copiedStyle && copiedStyle[qty - 1]) { - this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty - 1]) }; - } - else if (currentCharStyle) { - this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) }; - } - else { - delete this.styles[lineIndex + qty]; - } - qty--; - } - this._forceClearCache = true; - }, - - /** - * Inserts style object for a given line/char index - * @param {Number} lineIndex Index of a line - * @param {Number} charIndex Index of a char - * @param {Number} quantity number Style object to insert, if given - * @param {Array} copiedStyle array of style objects - */ - insertCharStyleObject: function (lineIndex, charIndex, quantity, copiedStyle) { - if (!this.styles) { - this.styles = {}; - } - var currentLineStyles = this.styles[lineIndex], - currentLineStylesCloned = currentLineStyles ? clone(currentLineStyles) : {}; - - quantity || (quantity = 1); - // shift all char styles by quantity forward - // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4 - for (var index in currentLineStylesCloned) { - var numericIndex = parseInt(index, 10); - if (numericIndex >= charIndex) { - currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex]; - // only delete the style if there was nothing moved there - if (!currentLineStylesCloned[numericIndex - quantity]) { - delete currentLineStyles[numericIndex]; - } - } - } - this._forceClearCache = true; - if (copiedStyle) { - while (quantity--) { - if (!Object.keys(copiedStyle[quantity]).length) { - continue; - } - if (!this.styles[lineIndex]) { - this.styles[lineIndex] = {}; - } - this.styles[lineIndex][charIndex + quantity] = clone(copiedStyle[quantity]); - } - return; - } - if (!currentLineStyles) { - return; - } - var newStyle = currentLineStyles[charIndex ? charIndex - 1 : 1]; - while (newStyle && quantity--) { - this.styles[lineIndex][charIndex + quantity] = clone(newStyle); - } - }, - - /** - * Inserts style object(s) - * @param {Array} insertedText Characters at the location where style is inserted - * @param {Number} start cursor index for inserting style - * @param {Array} [copiedStyle] array of style objects to insert. - */ - insertNewStyleBlock: function (insertedText, start, copiedStyle) { - var cursorLoc = this.get2DCursorLocation(start, true), - addedLines = [0], linesLength = 0; - // get an array of how many char per lines are being added. - for (var i = 0; i < insertedText.length; i++) { - if (insertedText[i] === '\n') { - linesLength++; - addedLines[linesLength] = 0; - } - else { - addedLines[linesLength]++; - } - } - // for the first line copy the style from the current char position. - if (addedLines[0] > 0) { - this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addedLines[0], copiedStyle); - copiedStyle = copiedStyle && copiedStyle.slice(addedLines[0] + 1); - } - linesLength && this.insertNewlineStyleObject( - cursorLoc.lineIndex, cursorLoc.charIndex + addedLines[0], linesLength); - for (var i = 1; i < linesLength; i++) { - if (addedLines[i] > 0) { - this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle); - } - else if (copiedStyle) { - // this test is required in order to close #6841 - // when a pasted buffer begins with a newline then - // this.styles[cursorLoc.lineIndex + i] and copiedStyle[0] - // may be undefined for some reason - if (this.styles[cursorLoc.lineIndex + i] && copiedStyle[0]) { - this.styles[cursorLoc.lineIndex + i][0] = copiedStyle[0]; - } - } - copiedStyle = copiedStyle && copiedStyle.slice(addedLines[i] + 1); - } - // we use i outside the loop to get it like linesLength - if (addedLines[i] > 0) { - this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle); - } - }, - - /** - * Set the selectionStart and selectionEnd according to the new position of cursor - * mimic the key - mouse navigation when shift is pressed. - */ - setSelectionStartEndWithShift: function (start, end, newSelection) { - if (newSelection <= start) { - if (end === start) { - this._selectionDirection = 'left'; - } - else if (this._selectionDirection === 'right') { - this._selectionDirection = 'left'; - this.selectionEnd = start; - } - this.selectionStart = newSelection; - } - else if (newSelection > start && newSelection < end) { - if (this._selectionDirection === 'right') { - this.selectionEnd = newSelection; - } - else { - this.selectionStart = newSelection; - } - } - else { - // newSelection is > selection start and end - if (end === start) { - this._selectionDirection = 'right'; - } - else if (this._selectionDirection === 'left') { - this._selectionDirection = 'right'; - this.selectionStart = end; - } - this.selectionEnd = newSelection; - } - }, - - setSelectionInBoundaries: function () { - var length = this.text.length; - if (this.selectionStart > length) { - this.selectionStart = length; - } - else if (this.selectionStart < 0) { - this.selectionStart = 0; - } - if (this.selectionEnd > length) { - this.selectionEnd = length; - } - else if (this.selectionEnd < 0) { - this.selectionEnd = 0; - } - } - }); -})(); - - -fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { - /** - * Initializes "dbclick" event handler - */ - initDoubleClickSimulation: function () { - - // for double click - this.__lastClickTime = +new Date(); - - // for triple click - this.__lastLastClickTime = +new Date(); - - this.__lastPointer = {}; - - this.on('mousedown', this.onMouseDown); - }, - - /** - * Default event handler to simulate triple click - * @private - */ - onMouseDown: function (options) { - if (!this.canvas) { - return; - } - this.__newClickTime = +new Date(); - var newPointer = options.pointer; - if (this.isTripleClick(newPointer)) { - this.fire('tripleclick', options); - this._stopEvent(options.e); - } - this.__lastLastClickTime = this.__lastClickTime; - this.__lastClickTime = this.__newClickTime; - this.__lastPointer = newPointer; - this.__lastIsEditing = this.isEditing; - this.__lastSelected = this.selected; - }, - - isTripleClick: function (newPointer) { - return this.__newClickTime - this.__lastClickTime < 500 && - this.__lastClickTime - this.__lastLastClickTime < 500 && - this.__lastPointer.x === newPointer.x && - this.__lastPointer.y === newPointer.y; - }, - - /** - * @private - */ - _stopEvent: function (e) { - e.preventDefault && e.preventDefault(); - e.stopPropagation && e.stopPropagation(); - }, - - /** - * Initializes event handlers related to cursor or selection - */ - initCursorSelectionHandlers: function () { - this.initMousedownHandler(); - this.initMouseupHandler(); - this.initClicks(); - }, - - /** - * Default handler for double click, select a word - */ - doubleClickHandler: function (options) { - if (!this.isEditing) { - return; - } - this.selectWord(this.getSelectionStartFromPointer(options.e)); - }, - - /** - * Default handler for triple click, select a line - */ - tripleClickHandler: function (options) { - if (!this.isEditing) { - return; - } - this.selectLine(this.getSelectionStartFromPointer(options.e)); - }, - - /** - * Initializes double and triple click event handlers - */ - initClicks: function () { - this.on('mousedblclick', this.doubleClickHandler); - this.on('tripleclick', this.tripleClickHandler); - }, - - /** - * Default event handler for the basic functionalities needed on _mouseDown - * can be overridden to do something different. - * Scope of this implementation is: find the click position, set selectionStart - * find selectionEnd, initialize the drawing of either cursor or selection area - * initializing a mousedDown on a text area will cancel fabricjs knowledge of - * current compositionMode. It will be set to false. - */ - _mouseDownHandler: function (options) { - if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { - return; - } - - this.__isMousedown = true; - - if (this.selected) { - this.inCompositionMode = false; - this.setCursorByClick(options.e); - } - - if (this.isEditing) { - this.__selectionStartOnMouseDown = this.selectionStart; - if (this.selectionStart === this.selectionEnd) { - this.abortCursorAnimation(); - } - this.renderCursorOrSelection(); - } - }, - - /** - * Default event handler for the basic functionalities needed on mousedown:before - * can be overridden to do something different. - * Scope of this implementation is: verify the object is already selected when mousing down - */ - _mouseDownHandlerBefore: function (options) { - if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { - return; - } - // we want to avoid that an object that was selected and then becomes unselectable, - // may trigger editing mode in some way. - this.selected = this === this.canvas._activeObject; - }, - - /** - * Initializes "mousedown" event handler - */ - initMousedownHandler: function () { - this.on('mousedown', this._mouseDownHandler); - this.on('mousedown:before', this._mouseDownHandlerBefore); - }, - - /** - * Initializes "mouseup" event handler - */ - initMouseupHandler: function () { - this.on('mouseup', this.mouseUpHandler); - }, - - /** - * standard handler for mouse up, overridable - * @private - */ - mouseUpHandler: function (options) { - this.__isMousedown = false; - if (!this.editable || this.group || - (options.transform && options.transform.actionPerformed) || - (options.e.button && options.e.button !== 1)) { - return; - } - - if (this.canvas) { - var currentActive = this.canvas._activeObject; - if (currentActive && currentActive !== this) { - // avoid running this logic when there is an active object - // this because is possible with shift click and fast clicks, - // to rapidly deselect and reselect this object and trigger an enterEdit - return; - } - } - - if (this.__lastSelected && !this.__corner) { - this.selected = false; - this.__lastSelected = false; - this.enterEditing(options.e); - if (this.selectionStart === this.selectionEnd) { - this.initDelayedCursor(true); - } - else { - this.renderCursorOrSelection(); - } - } - else { - this.selected = true; - } - }, - - /** - * Changes cursor location in a text depending on passed pointer (x/y) object - * @param {Event} e Event object - */ - setCursorByClick: function (e) { - var newSelection = this.getSelectionStartFromPointer(e), - start = this.selectionStart, end = this.selectionEnd; - if (e.shiftKey) { - this.setSelectionStartEndWithShift(start, end, newSelection); - } - else { - this.selectionStart = newSelection; - this.selectionEnd = newSelection; - } - if (this.isEditing) { - this._fireSelectionChanged(); - this._updateTextarea(); - } - }, - - /** - * Returns index of a character corresponding to where an object was clicked - * @param {Event} e Event object - * @return {Number} Index of a character - */ - getSelectionStartFromPointer: function (e) { - var mouseOffset = this.getLocalPointer(e), - prevWidth = 0, - width = 0, - height = 0, - charIndex = 0, - lineIndex = 0, - lineLeftOffset, - line; - for (var i = 0, len = this._textLines.length; i < len; i++) { - if (height <= mouseOffset.y) { - height += this.getHeightOfLine(i) * this.scaleY; - lineIndex = i; - if (i > 0) { - charIndex += this._textLines[i - 1].length + this.missingNewlineOffset(i - 1); - } - } - else { - break; - } - } - lineLeftOffset = this._getLineLeftOffset(lineIndex); - width = lineLeftOffset * this.scaleX; - line = this._textLines[lineIndex]; - // handling of RTL: in order to get things work correctly, - // we assume RTL writing is mirrored compared to LTR writing. - // so in position detection we mirror the X offset, and when is time - // of rendering it, we mirror it again. - if (this.direction === 'rtl') { - mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; - } - for (var j = 0, jlen = line.length; j < jlen; j++) { - prevWidth = width; - // i removed something about flipX here, check. - width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX; - if (width <= mouseOffset.x) { - charIndex++; - } - else { - break; - } - } - return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen); - }, - - /** - * @private - */ - _getNewSelectionStartFromOffset: function (mouseOffset, prevWidth, width, index, jlen) { - // we need Math.abs because when width is after the last char, the offset is given as 1, while is 0 - var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, - distanceBtwNextCharAndCursor = width - mouseOffset.x, - offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor || - distanceBtwNextCharAndCursor < 0 ? 0 : 1, - newSelectionStart = index + offset; - // if object is horizontally flipped, mirror cursor location from the end - if (this.flipX) { - newSelectionStart = jlen - newSelectionStart; - } - - if (newSelectionStart > this._text.length) { - newSelectionStart = this._text.length; - } - - return newSelectionStart; - } -}); - - -fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { - - /** - * Initializes hidden textarea (needed to bring up keyboard in iOS) - */ - initHiddenTextarea: function () { - this.hiddenTextarea = fabric.document.createElement('textarea'); - this.hiddenTextarea.setAttribute('autocapitalize', 'off'); - this.hiddenTextarea.setAttribute('autocorrect', 'off'); - this.hiddenTextarea.setAttribute('autocomplete', 'off'); - this.hiddenTextarea.setAttribute('spellcheck', 'false'); - this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', ''); - this.hiddenTextarea.setAttribute('wrap', 'off'); - var style = this._calcTextareaPosition(); - // line-height: 1px; was removed from the style to fix this: - // https://bugs.chromium.org/p/chromium/issues/detail?id=870966 - this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top + - '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' + - ' padding-top: ' + style.fontSize + ';'; - - if (this.hiddenTextareaContainer) { - this.hiddenTextareaContainer.appendChild(this.hiddenTextarea); - } - else { - fabric.document.body.appendChild(this.hiddenTextarea); - } - - fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); - fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this)); - fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this)); - fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this)); - fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this)); - fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this)); - fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this)); - fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this)); - fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this)); - - if (!this._clickHandlerInitialized && this.canvas) { - fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this)); - this._clickHandlerInitialized = true; - } - }, - - /** - * For functionalities on keyDown - * Map a special key to a function of the instance/prototype - * If you need different behaviour for ESC or TAB or arrows, you have to change - * this map setting the name of a function that you build on the fabric.Itext or - * your prototype. - * the map change will affect all Instances unless you need for only some text Instances - * in that case you have to clone this object and assign your Instance. - * this.keysMap = fabric.util.object.clone(this.keysMap); - * The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0] - */ - keysMap: { - 9: 'exitEditing', - 27: 'exitEditing', - 33: 'moveCursorUp', - 34: 'moveCursorDown', - 35: 'moveCursorRight', - 36: 'moveCursorLeft', - 37: 'moveCursorLeft', - 38: 'moveCursorUp', - 39: 'moveCursorRight', - 40: 'moveCursorDown', - }, - - keysMapRtl: { - 9: 'exitEditing', - 27: 'exitEditing', - 33: 'moveCursorUp', - 34: 'moveCursorDown', - 35: 'moveCursorLeft', - 36: 'moveCursorRight', - 37: 'moveCursorRight', - 38: 'moveCursorUp', - 39: 'moveCursorLeft', - 40: 'moveCursorDown', - }, - - /** - * For functionalities on keyUp + ctrl || cmd - */ - ctrlKeysMapUp: { - 67: 'copy', - 88: 'cut' - }, - - /** - * For functionalities on keyDown + ctrl || cmd - */ - ctrlKeysMapDown: { - 65: 'selectAll' - }, - - onClick: function () { - // No need to trigger click event here, focus is enough to have the keyboard appear on Android - this.hiddenTextarea && this.hiddenTextarea.focus(); - }, - - /** - * Handles keydown event - * only used for arrows and combination of modifier keys. - * @param {Event} e Event object - */ - onKeyDown: function (e) { - if (!this.isEditing) { - return; - } - var keyMap = this.direction === 'rtl' ? this.keysMapRtl : this.keysMap; - if (e.keyCode in keyMap) { - this[keyMap[e.keyCode]](e); - } - else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) { - this[this.ctrlKeysMapDown[e.keyCode]](e); - } - else { - return; - } - e.stopImmediatePropagation(); - e.preventDefault(); - if (e.keyCode >= 33 && e.keyCode <= 40) { - // if i press an arrow key just update selection - this.inCompositionMode = false; - this.clearContextTop(); - this.renderCursorOrSelection(); - } - else { - this.canvas && this.canvas.requestRenderAll(); - } - }, - - /** - * Handles keyup event - * We handle KeyUp because ie11 and edge have difficulties copy/pasting - * if a copy/cut event fired, keyup is dismissed - * @param {Event} e Event object - */ - onKeyUp: function (e) { - if (!this.isEditing || this._copyDone || this.inCompositionMode) { - this._copyDone = false; - return; - } - if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) { - this[this.ctrlKeysMapUp[e.keyCode]](e); - } - else { - return; - } - e.stopImmediatePropagation(); - e.preventDefault(); - this.canvas && this.canvas.requestRenderAll(); - }, - - /** - * Handles onInput event - * @param {Event} e Event object - */ - onInput: function (e) { - var fromPaste = this.fromPaste; - this.fromPaste = false; - e && e.stopPropagation(); - if (!this.isEditing) { - return; - } - // decisions about style changes. - var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText, - charCount = this._text.length, - nextCharCount = nextText.length, - removedText, insertedText, - charDiff = nextCharCount - charCount, - selectionStart = this.selectionStart, selectionEnd = this.selectionEnd, - selection = selectionStart !== selectionEnd, - copiedStyle, removeFrom, removeTo; - if (this.hiddenTextarea.value === '') { - this.styles = {}; - this.updateFromTextArea(); - this.fire('changed'); - if (this.canvas) { - this.canvas.fire('text:changed', { target: this }); - this.canvas.requestRenderAll(); - } - return; - } - - var textareaSelection = this.fromStringToGraphemeSelection( - this.hiddenTextarea.selectionStart, - this.hiddenTextarea.selectionEnd, - this.hiddenTextarea.value - ); - var backDelete = selectionStart > textareaSelection.selectionStart; - - if (selection) { - removedText = this._text.slice(selectionStart, selectionEnd); - charDiff += selectionEnd - selectionStart; - } - else if (nextCharCount < charCount) { - if (backDelete) { - removedText = this._text.slice(selectionEnd + charDiff, selectionEnd); - } - else { - removedText = this._text.slice(selectionStart, selectionStart - charDiff); - } - } - insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd); - if (removedText && removedText.length) { - if (insertedText.length) { - // let's copy some style before deleting. - // we want to copy the style before the cursor OR the style at the cursor if selection - // is bigger than 0. - copiedStyle = this.getSelectionStyles(selectionStart, selectionStart + 1, false); - // now duplicate the style one for each inserted text. - copiedStyle = insertedText.map(function () { - // this return an array of references, but that is fine since we are - // copying the style later. - return copiedStyle[0]; - }); - } - if (selection) { - removeFrom = selectionStart; - removeTo = selectionEnd; - } - else if (backDelete) { - // detect differences between forwardDelete and backDelete - removeFrom = selectionEnd - removedText.length; - removeTo = selectionEnd; - } - else { - removeFrom = selectionEnd; - removeTo = selectionEnd + removedText.length; - } - this.removeStyleFromTo(removeFrom, removeTo); - } - if (insertedText.length) { - if (fromPaste && insertedText.join('') === fabric.copiedText && !fabric.disableStyleCopyPaste) { - copiedStyle = fabric.copiedTextStyle; - } - this.insertNewStyleBlock(insertedText, selectionStart, copiedStyle); - } - this.updateFromTextArea(); - this.fire('changed'); - if (this.canvas) { - this.canvas.fire('text:changed', { target: this }); - this.canvas.requestRenderAll(); - } - }, - /** - * Composition start - */ - onCompositionStart: function () { - this.inCompositionMode = true; - }, - - /** - * Composition end - */ - onCompositionEnd: function () { - this.inCompositionMode = false; - }, - - // /** - // * Composition update - // */ - onCompositionUpdate: function (e) { - this.compositionStart = e.target.selectionStart; - this.compositionEnd = e.target.selectionEnd; - this.updateTextareaPosition(); - }, - - /** - * Copies selected text - * @param {Event} e Event object - */ - copy: function () { - if (this.selectionStart === this.selectionEnd) { - //do not cut-copy if no selection - return; - } - - fabric.copiedText = this.getSelectedText(); - if (!fabric.disableStyleCopyPaste) { - fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true); - } - else { - fabric.copiedTextStyle = null; - } - this._copyDone = true; - }, - - /** - * Pastes text - * @param {Event} e Event object - */ - paste: function () { - this.fromPaste = true; - }, - - /** - * @private - * @param {Event} e Event object - * @return {Object} Clipboard data object - */ - _getClipboardData: function (e) { - return (e && e.clipboardData) || fabric.window.clipboardData; - }, - - /** - * Finds the width in pixels before the cursor on the same line - * @private - * @param {Number} lineIndex - * @param {Number} charIndex - * @return {Number} widthBeforeCursor width before cursor - */ - _getWidthBeforeCursor: function (lineIndex, charIndex) { - var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound; - - if (charIndex > 0) { - bound = this.__charBounds[lineIndex][charIndex - 1]; - widthBeforeCursor += bound.left + bound.width; - } - return widthBeforeCursor; - }, - - /** - * Gets start offset of a selection - * @param {Event} e Event object - * @param {Boolean} isRight - * @return {Number} - */ - getDownCursorOffset: function (e, isRight) { - var selectionProp = this._getSelectionForOffset(e, isRight), - cursorLocation = this.get2DCursorLocation(selectionProp), - lineIndex = cursorLocation.lineIndex; - // if on last line, down cursor goes to end of line - if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) { - // move to the end of a text - return this._text.length - selectionProp; - } - var charIndex = cursorLocation.charIndex, - widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), - indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor), - textAfterCursor = this._textLines[lineIndex].slice(charIndex); - return textAfterCursor.length + indexOnOtherLine + 1 + this.missingNewlineOffset(lineIndex); - }, - - /** - * private - * Helps finding if the offset should be counted from Start or End - * @param {Event} e Event object - * @param {Boolean} isRight - * @return {Number} - */ - _getSelectionForOffset: function (e, isRight) { - if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) { - return this.selectionEnd; - } - else { - return this.selectionStart; - } - }, - - /** - * @param {Event} e Event object - * @param {Boolean} isRight - * @return {Number} - */ - getUpCursorOffset: function (e, isRight) { - var selectionProp = this._getSelectionForOffset(e, isRight), - cursorLocation = this.get2DCursorLocation(selectionProp), - lineIndex = cursorLocation.lineIndex; - if (lineIndex === 0 || e.metaKey || e.keyCode === 33) { - // if on first line, up cursor goes to start of line - return -selectionProp; - } - var charIndex = cursorLocation.charIndex, - widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), - indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor), - textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex), - missingNewlineOffset = this.missingNewlineOffset(lineIndex - 1); - // return a negative offset - return -this._textLines[lineIndex - 1].length - + indexOnOtherLine - textBeforeCursor.length + (1 - missingNewlineOffset); - }, - - /** - * for a given width it founds the matching character. - * @private - */ - _getIndexOnLine: function (lineIndex, width) { - - var line = this._textLines[lineIndex], - lineLeftOffset = this._getLineLeftOffset(lineIndex), - widthOfCharsOnLine = lineLeftOffset, - indexOnLine = 0, charWidth, foundMatch; - - for (var j = 0, jlen = line.length; j < jlen; j++) { - charWidth = this.__charBounds[lineIndex][j].width; - widthOfCharsOnLine += charWidth; - if (widthOfCharsOnLine > width) { - foundMatch = true; - var leftEdge = widthOfCharsOnLine - charWidth, - rightEdge = widthOfCharsOnLine, - offsetFromLeftEdge = Math.abs(leftEdge - width), - offsetFromRightEdge = Math.abs(rightEdge - width); - - indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); - break; - } - } - - // reached end - if (!foundMatch) { - indexOnLine = line.length - 1; - } - - return indexOnLine; - }, - - - /** - * Moves cursor down - * @param {Event} e Event object - */ - moveCursorDown: function (e) { - if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { - return; - } - this._moveCursorUpOrDown('Down', e); - }, - - /** - * Moves cursor up - * @param {Event} e Event object - */ - moveCursorUp: function (e) { - if (this.selectionStart === 0 && this.selectionEnd === 0) { - return; - } - this._moveCursorUpOrDown('Up', e); - }, - - /** - * Moves cursor up or down, fires the events - * @param {String} direction 'Up' or 'Down' - * @param {Event} e Event object - */ - _moveCursorUpOrDown: function (direction, e) { - // getUpCursorOffset - // getDownCursorOffset - var action = 'get' + direction + 'CursorOffset', - offset = this[action](e, this._selectionDirection === 'right'); - if (e.shiftKey) { - this.moveCursorWithShift(offset); - } - else { - this.moveCursorWithoutShift(offset); - } - if (offset !== 0) { - this.setSelectionInBoundaries(); - this.abortCursorAnimation(); - this._currentCursorOpacity = 1; - this.initDelayedCursor(); - this._fireSelectionChanged(); - this._updateTextarea(); - } - }, - - /** - * Moves cursor with shift - * @param {Number} offset - */ - moveCursorWithShift: function (offset) { - var newSelection = this._selectionDirection === 'left' - ? this.selectionStart + offset - : this.selectionEnd + offset; - this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection); - return offset !== 0; - }, - - /** - * Moves cursor up without shift - * @param {Number} offset - */ - moveCursorWithoutShift: function (offset) { - if (offset < 0) { - this.selectionStart += offset; - this.selectionEnd = this.selectionStart; - } - else { - this.selectionEnd += offset; - this.selectionStart = this.selectionEnd; - } - return offset !== 0; - }, - - /** - * Moves cursor left - * @param {Event} e Event object - */ - moveCursorLeft: function (e) { - if (this.selectionStart === 0 && this.selectionEnd === 0) { - return; - } - this._moveCursorLeftOrRight('Left', e); - }, - - /** - * @private - * @return {Boolean} true if a change happened - */ - _move: function (e, prop, direction) { - var newValue; - if (e.altKey) { - newValue = this['findWordBoundary' + direction](this[prop]); - } - else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36) { - newValue = this['findLineBoundary' + direction](this[prop]); - } - else { - this[prop] += direction === 'Left' ? -1 : 1; - return true; - } - if (typeof newValue !== 'undefined' && this[prop] !== newValue) { - this[prop] = newValue; - return true; - } - }, - - /** - * @private - */ - _moveLeft: function (e, prop) { - return this._move(e, prop, 'Left'); - }, - - /** - * @private - */ - _moveRight: function (e, prop) { - return this._move(e, prop, 'Right'); - }, - - /** - * Moves cursor left without keeping selection - * @param {Event} e - */ - moveCursorLeftWithoutShift: function (e) { - var change = true; - this._selectionDirection = 'left'; - - // only move cursor when there is no selection, - // otherwise we discard it, and leave cursor on same place - if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) { - change = this._moveLeft(e, 'selectionStart'); - - } - this.selectionEnd = this.selectionStart; - return change; - }, - - /** - * Moves cursor left while keeping selection - * @param {Event} e - */ - moveCursorLeftWithShift: function (e) { - if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { - return this._moveLeft(e, 'selectionEnd'); - } - else if (this.selectionStart !== 0) { - this._selectionDirection = 'left'; - return this._moveLeft(e, 'selectionStart'); - } - }, - - /** - * Moves cursor right - * @param {Event} e Event object - */ - moveCursorRight: function (e) { - if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { - return; - } - this._moveCursorLeftOrRight('Right', e); - }, - - /** - * Moves cursor right or Left, fires event - * @param {String} direction 'Left', 'Right' - * @param {Event} e Event object - */ - _moveCursorLeftOrRight: function (direction, e) { - var actionName = 'moveCursor' + direction + 'With'; - this._currentCursorOpacity = 1; - - if (e.shiftKey) { - actionName += 'Shift'; - } - else { - actionName += 'outShift'; - } - if (this[actionName](e)) { - this.abortCursorAnimation(); - this.initDelayedCursor(); - this._fireSelectionChanged(); - this._updateTextarea(); - } - }, - - /** - * Moves cursor right while keeping selection - * @param {Event} e - */ - moveCursorRightWithShift: function (e) { - if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { - return this._moveRight(e, 'selectionStart'); - } - else if (this.selectionEnd !== this._text.length) { - this._selectionDirection = 'right'; - return this._moveRight(e, 'selectionEnd'); - } - }, - - /** - * Moves cursor right without keeping selection - * @param {Event} e Event object - */ - moveCursorRightWithoutShift: function (e) { - var changed = true; - this._selectionDirection = 'right'; - - if (this.selectionStart === this.selectionEnd) { - changed = this._moveRight(e, 'selectionStart'); - this.selectionEnd = this.selectionStart; - } - else { - this.selectionStart = this.selectionEnd; - } - return changed; - }, - - /** - * Removes characters from start/end - * start/end ar per grapheme position in _text array. - * - * @param {Number} start - * @param {Number} end default to start + 1 - */ - removeChars: function (start, end) { - if (typeof end === 'undefined') { - end = start + 1; - } - this.removeStyleFromTo(start, end); - this._text.splice(start, end - start); - this.text = this._text.join(''); - this.set('dirty', true); - if (this._shouldClearDimensionCache()) { - this.initDimensions(); - this.setCoords(); - } - this._removeExtraneousStyles(); - }, - - /** - * insert characters at start position, before start position. - * start equal 1 it means the text get inserted between actual grapheme 0 and 1 - * if style array is provided, it must be as the same length of text in graphemes - * if end is provided and is bigger than start, old text is replaced. - * start/end ar per grapheme position in _text array. - * - * @param {String} text text to insert - * @param {Array} style array of style objects - * @param {Number} start - * @param {Number} end default to start + 1 - */ - insertChars: function (text, style, start, end) { - if (typeof end === 'undefined') { - end = start; - } - if (end > start) { - this.removeStyleFromTo(start, end); - } - var graphemes = fabric.util.string.graphemeSplit(text); - this.insertNewStyleBlock(graphemes, start, style); - this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end)); - this.text = this._text.join(''); - this.set('dirty', true); - if (this._shouldClearDimensionCache()) { - this.initDimensions(); - this.setCoords(); - } - this._removeExtraneousStyles(); - }, - -}); - - -/* _TO_SVG_START_ */ -(function () { - var toFixed = fabric.util.toFixed, - multipleSpacesRegex = / +/g; - - fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { - - /** - * Returns SVG representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - _toSVG: function () { - var offsets = this._getSVGLeftTopOffsets(), - textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft); - return this._wrapSVGTextAndBg(textAndBg); - }, - - /** - * Returns svg representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - toSVG: function (reviver) { - return this._createBaseSVGMarkup( - this._toSVG(), - { reviver: reviver, noStyle: true, withShadow: true } - ); - }, - - /** - * @private - */ - _getSVGLeftTopOffsets: function () { - return { - textLeft: -this.width / 2, - textTop: -this.height / 2, - lineTop: this.getHeightOfLine(0) - }; - }, - - /** - * @private - */ - _wrapSVGTextAndBg: function (textAndBg) { - var noShadow = true, - textDecoration = this.getSvgTextDecoration(this); - return [ - textAndBg.textBgRects.join(''), - '\t\t', - textAndBg.textSpans.join(''), - '\n' - ]; - }, - - /** - * @private - * @param {Number} textTopOffset Text top offset - * @param {Number} textLeftOffset Text left offset - * @return {Object} - */ - _getSVGTextAndBg: function (textTopOffset, textLeftOffset) { - var textSpans = [], - textBgRects = [], - height = textTopOffset, lineOffset; - // bounding-box background - this._setSVGBg(textBgRects); - - // text and text-background - for (var i = 0, len = this._textLines.length; i < len; i++) { - lineOffset = this._getLineLeftOffset(i); - if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { - this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); - } - this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height); - height += this.getHeightOfLine(i); - } - - return { - textSpans: textSpans, - textBgRects: textBgRects - }; - }, - - /** - * @private - */ - _createTextCharSpan: function (_char, styleDecl, left, top) { - var shouldUseWhitespace = _char !== _char.trim() || _char.match(multipleSpacesRegex), - styleProps = this.getSvgSpanStyles(styleDecl, shouldUseWhitespace), - fillStyles = styleProps ? 'style="' + styleProps + '"' : '', - dy = styleDecl.deltaY, dySpan = '', - NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - if (dy) { - dySpan = ' dy="' + toFixed(dy, NUM_FRACTION_DIGITS) + '" '; - } - return [ - '', - fabric.util.string.escapeXml(_char), - '' - ].join(''); - }, - - _setSVGTextLineText: function (textSpans, lineIndex, textLeftOffset, textTopOffset) { - // set proper line offset - var lineHeight = this.getHeightOfLine(lineIndex), - isJustify = this.textAlign.indexOf('justify') !== -1, - actualStyle, - nextStyle, - charsToRender = '', - charBox, style, - boxWidth = 0, - line = this._textLines[lineIndex], - timeToRender; - - textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight; - for (var i = 0, len = line.length - 1; i <= len; i++) { - timeToRender = i === len || this.charSpacing; - charsToRender += line[i]; - charBox = this.__charBounds[lineIndex][i]; - if (boxWidth === 0) { - textLeftOffset += charBox.kernedWidth - charBox.width; - boxWidth += charBox.width; - } - else { - boxWidth += charBox.kernedWidth; - } - if (isJustify && !timeToRender) { - if (this._reSpaceAndTab.test(line[i])) { - timeToRender = true; - } - } - if (!timeToRender) { - // if we have charSpacing, we render char by char - actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); - nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); - timeToRender = fabric.util.hasStyleChanged(actualStyle, nextStyle, true); - } - if (timeToRender) { - style = this._getStyleDeclaration(lineIndex, i) || {}; - textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); - charsToRender = ''; - actualStyle = nextStyle; - textLeftOffset += boxWidth; - boxWidth = 0; - } - } - }, - - _pushTextBgRect: function (textBgRects, color, left, top, width, height) { - var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - textBgRects.push( - '\t\t\n'); - }, - - _setSVGTextLineBg: function (textBgRects, i, leftOffset, textTopOffset) { - var line = this._textLines[i], - heightOfLine = this.getHeightOfLine(i) / this.lineHeight, - boxWidth = 0, - boxStart = 0, - charBox, currentColor, - lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); - for (var j = 0, jlen = line.length; j < jlen; j++) { - charBox = this.__charBounds[i][j]; - currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); - if (currentColor !== lastColor) { - lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart, - textTopOffset, boxWidth, heightOfLine); - boxStart = charBox.left; - boxWidth = charBox.width; - lastColor = currentColor; - } - else { - boxWidth += charBox.kernedWidth; - } - } - currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart, - textTopOffset, boxWidth, heightOfLine); - }, - - /** - * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values - * we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 - * - * @private - * @param {*} value - * @return {String} - */ - _getFillAttributes: function (value) { - var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : ''; - if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { - return 'fill="' + value + '"'; - } - return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; - }, - - /** - * @private - */ - _getSVGLineTopOffset: function (lineIndex) { - var lineTopOffset = 0, lastHeight = 0; - for (var j = 0; j < lineIndex; j++) { - lineTopOffset += this.getHeightOfLine(j); - } - lastHeight = this.getHeightOfLine(j); - return { - lineTop: lineTopOffset, - offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult) - }; - }, - - /** - * Returns styles-string for svg-export - * @param {Boolean} skipShadow a boolean to skip shadow filter output - * @return {String} - */ - getSvgStyles: function (skipShadow) { - var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow); - return svgStyle + ' white-space: pre;'; - }, - }); -})(); -/* _TO_SVG_END_ */ - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}); - - /** - * Textbox class, based on IText, allows the user to resize the text rectangle - * and wraps lines automatically. Textboxes have their Y scaling locked, the - * user can only change width. Height is adjusted automatically based on the - * wrapping of lines. - * @class fabric.Textbox - * @extends fabric.IText - * @mixes fabric.Observable - * @return {fabric.Textbox} thisArg - * @see {@link fabric.Textbox#initialize} for constructor definition - */ - fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, { - - /** - * Type of an object - * @type String - * @default - */ - type: 'textbox', - - /** - * Minimum width of textbox, in pixels. - * @type Number - * @default - */ - minWidth: 20, - - /** - * Minimum calculated width of a textbox, in pixels. - * fixed to 2 so that an empty textbox cannot go to 0 - * and is still selectable without text. - * @type Number - * @default - */ - dynamicMinWidth: 2, - - /** - * Cached array of text wrapping. - * @type Array - */ - __cachedLines: null, - - /** - * Override standard Object class values - */ - lockScalingFlip: true, - - /** - * Override standard Object class values - * Textbox needs this on false - */ - noScaleCache: false, - - /** - * Properties which when set cause object to change dimensions - * @type Object - * @private - */ - _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'), - - /** - * Use this regular expression to split strings in breakable lines - * @private - */ - _wordJoiners: /[ \t\r]/, - - /** - * Use this boolean property in order to split strings that have no white space concept. - * this is a cheap way to help with chinese/japanese - * @type Boolean - * @since 2.6.0 - */ - splitByGrapheme: false, - - /** - * Unlike superclass's version of this function, Textbox does not update - * its width. - * @private - * @override - */ - initDimensions: function () { - if (this.__skipDimension) { - return; - } - this.isEditing && this.initDelayedCursor(); - this.clearContextTop(); - this._clearCache(); - // clear dynamicMinWidth as it will be different after we re-wrap line - this.dynamicMinWidth = 0; - // wrap lines - this._styleMap = this._generateStyleMap(this._splitText()); - // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap - if (this.dynamicMinWidth > this.width) { - this._set('width', this.dynamicMinWidth); - } - if (this.textAlign.indexOf('justify') !== -1) { - // once text is measured we need to make space fatter to make justified text. - this.enlargeSpaces(); - } - // clear cache and re-calculate height - this.height = this.calcTextHeight(); - this.saveState({ propertySet: '_dimensionAffectingProps' }); - }, - - /** - * Generate an object that translates the style object so that it is - * broken up by visual lines (new lines and automatic wrapping). - * The original text styles object is broken up by actual lines (new lines only), - * which is only sufficient for Text / IText - * @private - */ - _generateStyleMap: function (textInfo) { - var realLineCount = 0, - realLineCharCount = 0, - charCount = 0, - map = {}; - - for (var i = 0; i < textInfo.graphemeLines.length; i++) { - if (textInfo.graphemeText[charCount] === '\n' && i > 0) { - realLineCharCount = 0; - charCount++; - realLineCount++; - } - else if (!this.splitByGrapheme && this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) { - // this case deals with space's that are removed from end of lines when wrapping - realLineCharCount++; - charCount++; - } - - map[i] = { line: realLineCount, offset: realLineCharCount }; - - charCount += textInfo.graphemeLines[i].length; - realLineCharCount += textInfo.graphemeLines[i].length; - } - - return map; - }, - - /** - * Returns true if object has a style property or has it on a specified line - * @param {Number} lineIndex - * @return {Boolean} - */ - styleHas: function (property, lineIndex) { - if (this._styleMap && !this.isWrapping) { - var map = this._styleMap[lineIndex]; - if (map) { - lineIndex = map.line; - } - } - return fabric.Text.prototype.styleHas.call(this, property, lineIndex); - }, - - /** - * Returns true if object has no styling or no styling in a line - * @param {Number} lineIndex , lineIndex is on wrapped lines. - * @return {Boolean} - */ - isEmptyStyles: function (lineIndex) { - if (!this.styles) { - return true; - } - var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false, - map = this._styleMap[lineIndex], mapNextLine = this._styleMap[lineIndex + 1]; - if (map) { - lineIndex = map.line; - offset = map.offset; - } - if (mapNextLine) { - nextLineIndex = mapNextLine.line; - shouldLimit = nextLineIndex === lineIndex; - nextOffset = mapNextLine.offset; - } - obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; - for (var p1 in obj) { - for (var p2 in obj[p1]) { - if (p2 >= offset && (!shouldLimit || p2 < nextOffset)) { - // eslint-disable-next-line no-unused-vars - for (var p3 in obj[p1][p2]) { - return false; - } - } - } - } - return true; - }, - - /** - * @param {Number} lineIndex - * @param {Number} charIndex - * @private - */ - _getStyleDeclaration: function (lineIndex, charIndex) { - if (this._styleMap && !this.isWrapping) { - var map = this._styleMap[lineIndex]; - if (!map) { - return null; - } - lineIndex = map.line; - charIndex = map.offset + charIndex; - } - return this.callSuper('_getStyleDeclaration', lineIndex, charIndex); - }, - - /** - * @param {Number} lineIndex - * @param {Number} charIndex - * @param {Object} style - * @private - */ - _setStyleDeclaration: function (lineIndex, charIndex, style) { - var map = this._styleMap[lineIndex]; - lineIndex = map.line; - charIndex = map.offset + charIndex; - - this.styles[lineIndex][charIndex] = style; - }, - - /** - * @param {Number} lineIndex - * @param {Number} charIndex - * @private - */ - _deleteStyleDeclaration: function (lineIndex, charIndex) { - var map = this._styleMap[lineIndex]; - lineIndex = map.line; - charIndex = map.offset + charIndex; - delete this.styles[lineIndex][charIndex]; - }, - - /** - * probably broken need a fix - * Returns the real style line that correspond to the wrapped lineIndex line - * Used just to verify if the line does exist or not. - * @param {Number} lineIndex - * @returns {Boolean} if the line exists or not - * @private - */ - _getLineStyle: function (lineIndex) { - var map = this._styleMap[lineIndex]; - return !!this.styles[map.line]; - }, - - /** - * Set the line style to an empty object so that is initialized - * @param {Number} lineIndex - * @param {Object} style - * @private - */ - _setLineStyle: function (lineIndex) { - var map = this._styleMap[lineIndex]; - this.styles[map.line] = {}; - }, - - /** - * Wraps text using the 'width' property of Textbox. First this function - * splits text on newlines, so we preserve newlines entered by the user. - * Then it wraps each line using the width of the Textbox by calling - * _wrapLine(). - * @param {Array} lines The string array of text that is split into lines - * @param {Number} desiredWidth width you want to wrap to - * @returns {Array} Array of lines - */ - _wrapText: function (lines, desiredWidth) { - var wrapped = [], i; - this.isWrapping = true; - for (i = 0; i < lines.length; i++) { - wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth)); - } - this.isWrapping = false; - return wrapped; - }, - - /** - * Helper function to measure a string of text, given its lineIndex and charIndex offset - * it gets called when charBounds are not available yet. - * @param {CanvasRenderingContext2D} ctx - * @param {String} text - * @param {number} lineIndex - * @param {number} charOffset - * @returns {number} - * @private - */ - _measureWord: function (word, lineIndex, charOffset) { - var width = 0, prevGrapheme, skipLeft = true; - charOffset = charOffset || 0; - for (var i = 0, len = word.length; i < len; i++) { - var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft); - width += box.kernedWidth; - prevGrapheme = word[i]; - } - return width; - }, - - /** - * Wraps a line of text using the width of the Textbox and a context. - * @param {Array} line The grapheme array that represent the line - * @param {Number} lineIndex - * @param {Number} desiredWidth width you want to wrap the line to - * @param {Number} reservedSpace space to remove from wrapping for custom functionalities - * @returns {Array} Array of line(s) into which the given text is wrapped - * to. - */ - _wrapLine: function (_line, lineIndex, desiredWidth, reservedSpace) { - var lineWidth = 0, - splitByGrapheme = this.splitByGrapheme, - graphemeLines = [], - line = [], - // spaces in different languages? - words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners), - word = '', - offset = 0, - infix = splitByGrapheme ? '' : ' ', - wordWidth = 0, - infixWidth = 0, - largestWordWidth = 0, - lineJustStarted = true, - additionalSpace = this._getWidthOfCharSpacing(), - reservedSpace = reservedSpace || 0; - // fix a difference between split and graphemeSplit - if (words.length === 0) { - words.push([]); - } - desiredWidth -= reservedSpace; - for (var i = 0; i < words.length; i++) { - // if using splitByGrapheme words are already in graphemes. - word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]); - wordWidth = this._measureWord(word, lineIndex, offset); - offset += word.length; - - lineWidth += infixWidth + wordWidth - additionalSpace; - if (lineWidth > desiredWidth && !lineJustStarted) { - graphemeLines.push(line); - line = []; - lineWidth = wordWidth; - lineJustStarted = true; - } - else { - lineWidth += additionalSpace; - } - - if (!lineJustStarted && !splitByGrapheme) { - line.push(infix); - } - line = line.concat(word); - - infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset); - offset++; - lineJustStarted = false; - // keep track of largest word - if (wordWidth > largestWordWidth) { - largestWordWidth = wordWidth; - } - } - - i && graphemeLines.push(line); - - if (largestWordWidth + reservedSpace > this.dynamicMinWidth) { - this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace; - } - return graphemeLines; - }, - - /** - * Detect if the text line is ended with an hard break - * text and itext do not have wrapping, return false - * @param {Number} lineIndex text to split - * @return {Boolean} - */ - isEndOfWrapping: function (lineIndex) { - if (!this._styleMap[lineIndex + 1]) { - // is last line, return true; - return true; - } - if (this._styleMap[lineIndex + 1].line !== this._styleMap[lineIndex].line) { - // this is last line before a line break, return true; - return true; - } - return false; - }, - - /** - * Detect if a line has a linebreak and so we need to account for it when moving - * and counting style. - * @return Number - */ - missingNewlineOffset: function (lineIndex) { - if (this.splitByGrapheme) { - return this.isEndOfWrapping(lineIndex) ? 1 : 0; - } - return 1; - }, - - /** - * Gets lines of text to render in the Textbox. This function calculates - * text wrapping on the fly every time it is called. - * @param {String} text text to split - * @returns {Array} Array of lines in the Textbox. - * @override - */ - _splitTextIntoLines: function (text) { - var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text), - graphemeLines = this._wrapText(newText.lines, this.width), - lines = new Array(graphemeLines.length); - for (var i = 0; i < graphemeLines.length; i++) { - lines[i] = graphemeLines[i].join(''); - } - newText.lines = lines; - newText.graphemeLines = graphemeLines; - return newText; - }, - - getMinWidth: function () { - return Math.max(this.minWidth, this.dynamicMinWidth); - }, - - _removeExtraneousStyles: function () { - var linesToKeep = {}; - for (var prop in this._styleMap) { - if (this._textLines[prop]) { - linesToKeep[this._styleMap[prop].line] = 1; - } - } - for (var prop in this.styles) { - if (!linesToKeep[prop]) { - delete this.styles[prop]; - } - } - }, - - /** - * Returns object representation of an instance - * @method toObject - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - return this.callSuper('toObject', ['minWidth', 'splitByGrapheme'].concat(propertiesToInclude)); - } - }); - - /** - * Returns fabric.Textbox instance from an object representation - * @static - * @memberOf fabric.Textbox - * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created - */ - fabric.Textbox.fromObject = function (object, callback) { - var styles = fabric.util.stylesFromArray(object.styles, object.text); - //copy object to prevent mutation - var objCopy = Object.assign({}, object, { styles: styles }); - return fabric.Object._fromObject('Textbox', objCopy, callback, 'text'); - }; -})(typeof exports !== 'undefined' ? exports : this); - - -(function () { - - var controlsUtils = fabric.controlsUtils, - scaleSkewStyleHandler = controlsUtils.scaleSkewCursorStyleHandler, - scaleStyleHandler = controlsUtils.scaleCursorStyleHandler, - scalingEqually = controlsUtils.scalingEqually, - scalingYOrSkewingX = controlsUtils.scalingYOrSkewingX, - scalingXOrSkewingY = controlsUtils.scalingXOrSkewingY, - scaleOrSkewActionName = controlsUtils.scaleOrSkewActionName, - objectControls = fabric.Object.prototype.controls; - - objectControls.ml = new fabric.Control({ - x: -0.5, - y: 0, - cursorStyleHandler: scaleSkewStyleHandler, - actionHandler: scalingXOrSkewingY, - getActionName: scaleOrSkewActionName, - }); - - objectControls.mr = new fabric.Control({ - x: 0.5, - y: 0, - cursorStyleHandler: scaleSkewStyleHandler, - actionHandler: scalingXOrSkewingY, - getActionName: scaleOrSkewActionName, - }); - - objectControls.mb = new fabric.Control({ - x: 0, - y: 0.5, - cursorStyleHandler: scaleSkewStyleHandler, - actionHandler: scalingYOrSkewingX, - getActionName: scaleOrSkewActionName, - }); - - objectControls.mt = new fabric.Control({ - x: 0, - y: -0.5, - cursorStyleHandler: scaleSkewStyleHandler, - actionHandler: scalingYOrSkewingX, - getActionName: scaleOrSkewActionName, - }); - - objectControls.tl = new fabric.Control({ - x: -0.5, - y: -0.5, - cursorStyleHandler: scaleStyleHandler, - actionHandler: scalingEqually - }); - - objectControls.tr = new fabric.Control({ - x: 0.5, - y: -0.5, - cursorStyleHandler: scaleStyleHandler, - actionHandler: scalingEqually - }); - - objectControls.bl = new fabric.Control({ - x: -0.5, - y: 0.5, - cursorStyleHandler: scaleStyleHandler, - actionHandler: scalingEqually - }); - - objectControls.br = new fabric.Control({ - x: 0.5, - y: 0.5, - cursorStyleHandler: scaleStyleHandler, - actionHandler: scalingEqually - }); - - objectControls.mtr = new fabric.Control({ - x: 0, - y: -0.5, - actionHandler: controlsUtils.rotationWithSnapping, - cursorStyleHandler: controlsUtils.rotationStyleHandler, - offsetY: -40, - withConnection: true, - actionName: 'rotate', - }); - - if (fabric.Textbox) { - // this is breaking the prototype inheritance, no time / ideas to fix it. - // is important to document that if you want to have all objects to have a - // specific custom control, you have to add it to Object prototype and to Textbox - // prototype. The controls are shared as references. So changes to control `tr` - // can still apply to all objects if needed. - var textBoxControls = fabric.Textbox.prototype.controls = {}; - - textBoxControls.mtr = objectControls.mtr; - textBoxControls.tr = objectControls.tr; - textBoxControls.br = objectControls.br; - textBoxControls.tl = objectControls.tl; - textBoxControls.bl = objectControls.bl; - textBoxControls.mt = objectControls.mt; - textBoxControls.mb = objectControls.mb; - - textBoxControls.mr = new fabric.Control({ - x: 0.5, - y: 0, - actionHandler: controlsUtils.changeWidth, - cursorStyleHandler: scaleSkewStyleHandler, - actionName: 'resizing', - }); - - textBoxControls.ml = new fabric.Control({ - x: -0.5, - y: 0, - actionHandler: controlsUtils.changeWidth, - cursorStyleHandler: scaleSkewStyleHandler, - actionName: 'resizing', - }); - } -})(); diff --git a/netbox_floorplan/static/netbox_floorplan/vendors/jq.js b/netbox_floorplan/static/netbox_floorplan/vendors/jq.js deleted file mode 100644 index 54e0d01..0000000 --- a/netbox_floorplan/static/netbox_floorplan/vendors/jq.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.7.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function (e, t) { "use strict"; "object" == typeof module && "object" == typeof module.exports ? module.exports = e.document ? t(e, !0) : function (e) { if (!e.document) throw new Error("jQuery requires a window with a document"); return t(e) } : t(e) }("undefined" != typeof window ? window : this, function (ie, e) { "use strict"; var oe = [], r = Object.getPrototypeOf, ae = oe.slice, g = oe.flat ? function (e) { return oe.flat.call(e) } : function (e) { return oe.concat.apply([], e) }, s = oe.push, se = oe.indexOf, n = {}, i = n.toString, ue = n.hasOwnProperty, o = ue.toString, a = o.call(Object), le = {}, v = function (e) { return "function" == typeof e && "number" != typeof e.nodeType && "function" != typeof e.item }, y = function (e) { return null != e && e === e.window }, C = ie.document, u = { type: !0, src: !0, nonce: !0, noModule: !0 }; function m(e, t, n) { var r, i, o = (n = n || C).createElement("script"); if (o.text = e, t) for (r in u) (i = t[r] || t.getAttribute && t.getAttribute(r)) && o.setAttribute(r, i); n.head.appendChild(o).parentNode.removeChild(o) } function x(e) { return null == e ? e + "" : "object" == typeof e || "function" == typeof e ? n[i.call(e)] || "object" : typeof e } var t = "3.7.0", l = /HTML$/i, ce = function (e, t) { return new ce.fn.init(e, t) }; function c(e) { var t = !!e && "length" in e && e.length, n = x(e); return !v(e) && !y(e) && ("array" === n || 0 === t || "number" == typeof t && 0 < t && t - 1 in e) } function fe(e, t) { return e.nodeName && e.nodeName.toLowerCase() === t.toLowerCase() } ce.fn = ce.prototype = { jquery: t, constructor: ce, length: 0, toArray: function () { return ae.call(this) }, get: function (e) { return null == e ? ae.call(this) : e < 0 ? this[e + this.length] : this[e] }, pushStack: function (e) { var t = ce.merge(this.constructor(), e); return t.prevObject = this, t }, each: function (e) { return ce.each(this, e) }, map: function (n) { return this.pushStack(ce.map(this, function (e, t) { return n.call(e, t, e) })) }, slice: function () { return this.pushStack(ae.apply(this, arguments)) }, first: function () { return this.eq(0) }, last: function () { return this.eq(-1) }, even: function () { return this.pushStack(ce.grep(this, function (e, t) { return (t + 1) % 2 })) }, odd: function () { return this.pushStack(ce.grep(this, function (e, t) { return t % 2 })) }, eq: function (e) { var t = this.length, n = +e + (e < 0 ? t : 0); return this.pushStack(0 <= n && n < t ? [this[n]] : []) }, end: function () { return this.prevObject || this.constructor() }, push: s, sort: oe.sort, splice: oe.splice }, ce.extend = ce.fn.extend = function () { var e, t, n, r, i, o, a = arguments[0] || {}, s = 1, u = arguments.length, l = !1; for ("boolean" == typeof a && (l = a, a = arguments[s] || {}, s++), "object" == typeof a || v(a) || (a = {}), s === u && (a = this, s--); s < u; s++)if (null != (e = arguments[s])) for (t in e) r = e[t], "__proto__" !== t && a !== r && (l && r && (ce.isPlainObject(r) || (i = Array.isArray(r))) ? (n = a[t], o = i && !Array.isArray(n) ? [] : i || ce.isPlainObject(n) ? n : {}, i = !1, a[t] = ce.extend(l, o, r)) : void 0 !== r && (a[t] = r)); return a }, ce.extend({ expando: "jQuery" + (t + Math.random()).replace(/\D/g, ""), isReady: !0, error: function (e) { throw new Error(e) }, noop: function () { }, isPlainObject: function (e) { var t, n; return !(!e || "[object Object]" !== i.call(e)) && (!(t = r(e)) || "function" == typeof (n = ue.call(t, "constructor") && t.constructor) && o.call(n) === a) }, isEmptyObject: function (e) { var t; for (t in e) return !1; return !0 }, globalEval: function (e, t, n) { m(e, { nonce: t && t.nonce }, n) }, each: function (e, t) { var n, r = 0; if (c(e)) { for (n = e.length; r < n; r++)if (!1 === t.call(e[r], r, e[r])) break } else for (r in e) if (!1 === t.call(e[r], r, e[r])) break; return e }, text: function (e) { var t, n = "", r = 0, i = e.nodeType; if (i) { if (1 === i || 9 === i || 11 === i) return e.textContent; if (3 === i || 4 === i) return e.nodeValue } else while (t = e[r++]) n += ce.text(t); return n }, makeArray: function (e, t) { var n = t || []; return null != e && (c(Object(e)) ? ce.merge(n, "string" == typeof e ? [e] : e) : s.call(n, e)), n }, inArray: function (e, t, n) { return null == t ? -1 : se.call(t, e, n) }, isXMLDoc: function (e) { var t = e && e.namespaceURI, n = e && (e.ownerDocument || e).documentElement; return !l.test(t || n && n.nodeName || "HTML") }, merge: function (e, t) { for (var n = +t.length, r = 0, i = e.length; r < n; r++)e[i++] = t[r]; return e.length = i, e }, grep: function (e, t, n) { for (var r = [], i = 0, o = e.length, a = !n; i < o; i++)!t(e[i], i) !== a && r.push(e[i]); return r }, map: function (e, t, n) { var r, i, o = 0, a = []; if (c(e)) for (r = e.length; o < r; o++)null != (i = t(e[o], o, n)) && a.push(i); else for (o in e) null != (i = t(e[o], o, n)) && a.push(i); return g(a) }, guid: 1, support: le }), "function" == typeof Symbol && (ce.fn[Symbol.iterator] = oe[Symbol.iterator]), ce.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "), function (e, t) { n["[object " + t + "]"] = t.toLowerCase() }); var pe = oe.pop, de = oe.sort, he = oe.splice, ge = "[\\x20\\t\\r\\n\\f]", ve = new RegExp("^" + ge + "+|((?:^|[^\\\\])(?:\\\\.)*)" + ge + "+$", "g"); ce.contains = function (e, t) { var n = t && t.parentNode; return e === n || !(!n || 1 !== n.nodeType || !(e.contains ? e.contains(n) : e.compareDocumentPosition && 16 & e.compareDocumentPosition(n))) }; var f = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; function p(e, t) { return t ? "\0" === e ? "\ufffd" : e.slice(0, -1) + "\\" + e.charCodeAt(e.length - 1).toString(16) + " " : "\\" + e } ce.escapeSelector = function (e) { return (e + "").replace(f, p) }; var ye = C, me = s; !function () { var e, b, w, o, a, T, r, C, d, i, k = me, S = ce.expando, E = 0, n = 0, s = W(), c = W(), u = W(), h = W(), l = function (e, t) { return e === t && (a = !0), 0 }, f = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", t = "(?:\\\\[\\da-fA-F]{1,6}" + ge + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", p = "\\[" + ge + "*(" + t + ")(?:" + ge + "*([*^$|!~]?=)" + ge + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + t + "))|)" + ge + "*\\]", g = ":(" + t + ")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|" + p + ")*)|.*)\\)|)", v = new RegExp(ge + "+", "g"), y = new RegExp("^" + ge + "*," + ge + "*"), m = new RegExp("^" + ge + "*([>+~]|" + ge + ")" + ge + "*"), x = new RegExp(ge + "|>"), j = new RegExp(g), A = new RegExp("^" + t + "$"), D = { ID: new RegExp("^#(" + t + ")"), CLASS: new RegExp("^\\.(" + t + ")"), TAG: new RegExp("^(" + t + "|[*])"), ATTR: new RegExp("^" + p), PSEUDO: new RegExp("^" + g), CHILD: new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + ge + "*(even|odd|(([+-]|)(\\d*)n|)" + ge + "*(?:([+-]|)" + ge + "*(\\d+)|))" + ge + "*\\)|)", "i"), bool: new RegExp("^(?:" + f + ")$", "i"), needsContext: new RegExp("^" + ge + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + ge + "*((?:-\\d)?\\d*)" + ge + "*\\)|)(?=[^-]|$)", "i") }, N = /^(?:input|select|textarea|button)$/i, q = /^h\d$/i, L = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, H = /[+~]/, O = new RegExp("\\\\[\\da-fA-F]{1,6}" + ge + "?|\\\\([^\\r\\n\\f])", "g"), P = function (e, t) { var n = "0x" + e.slice(1) - 65536; return t || (n < 0 ? String.fromCharCode(n + 65536) : String.fromCharCode(n >> 10 | 55296, 1023 & n | 56320)) }, R = function () { V() }, M = J(function (e) { return !0 === e.disabled && fe(e, "fieldset") }, { dir: "parentNode", next: "legend" }); try { k.apply(oe = ae.call(ye.childNodes), ye.childNodes), oe[ye.childNodes.length].nodeType } catch (e) { k = { apply: function (e, t) { me.apply(e, ae.call(t)) }, call: function (e) { me.apply(e, ae.call(arguments, 1)) } } } function I(t, e, n, r) { var i, o, a, s, u, l, c, f = e && e.ownerDocument, p = e ? e.nodeType : 9; if (n = n || [], "string" != typeof t || !t || 1 !== p && 9 !== p && 11 !== p) return n; if (!r && (V(e), e = e || T, C)) { if (11 !== p && (u = L.exec(t))) if (i = u[1]) { if (9 === p) { if (!(a = e.getElementById(i))) return n; if (a.id === i) return k.call(n, a), n } else if (f && (a = f.getElementById(i)) && I.contains(e, a) && a.id === i) return k.call(n, a), n } else { if (u[2]) return k.apply(n, e.getElementsByTagName(t)), n; if ((i = u[3]) && e.getElementsByClassName) return k.apply(n, e.getElementsByClassName(i)), n } if (!(h[t + " "] || d && d.test(t))) { if (c = t, f = e, 1 === p && (x.test(t) || m.test(t))) { (f = H.test(t) && z(e.parentNode) || e) == e && le.scope || ((s = e.getAttribute("id")) ? s = ce.escapeSelector(s) : e.setAttribute("id", s = S)), o = (l = Y(t)).length; while (o--) l[o] = (s ? "#" + s : ":scope") + " " + Q(l[o]); c = l.join(",") } try { return k.apply(n, f.querySelectorAll(c)), n } catch (e) { h(t, !0) } finally { s === S && e.removeAttribute("id") } } } return re(t.replace(ve, "$1"), e, n, r) } function W() { var r = []; return function e(t, n) { return r.push(t + " ") > b.cacheLength && delete e[r.shift()], e[t + " "] = n } } function F(e) { return e[S] = !0, e } function $(e) { var t = T.createElement("fieldset"); try { return !!e(t) } catch (e) { return !1 } finally { t.parentNode && t.parentNode.removeChild(t), t = null } } function B(t) { return function (e) { return fe(e, "input") && e.type === t } } function _(t) { return function (e) { return (fe(e, "input") || fe(e, "button")) && e.type === t } } function X(t) { return function (e) { return "form" in e ? e.parentNode && !1 === e.disabled ? "label" in e ? "label" in e.parentNode ? e.parentNode.disabled === t : e.disabled === t : e.isDisabled === t || e.isDisabled !== !t && M(e) === t : e.disabled === t : "label" in e && e.disabled === t } } function U(a) { return F(function (o) { return o = +o, F(function (e, t) { var n, r = a([], e.length, o), i = r.length; while (i--) e[n = r[i]] && (e[n] = !(t[n] = e[n])) }) }) } function z(e) { return e && "undefined" != typeof e.getElementsByTagName && e } function V(e) { var t, n = e ? e.ownerDocument || e : ye; return n != T && 9 === n.nodeType && n.documentElement && (r = (T = n).documentElement, C = !ce.isXMLDoc(T), i = r.matches || r.webkitMatchesSelector || r.msMatchesSelector, ye != T && (t = T.defaultView) && t.top !== t && t.addEventListener("unload", R), le.getById = $(function (e) { return r.appendChild(e).id = ce.expando, !T.getElementsByName || !T.getElementsByName(ce.expando).length }), le.disconnectedMatch = $(function (e) { return i.call(e, "*") }), le.scope = $(function () { return T.querySelectorAll(":scope") }), le.cssHas = $(function () { try { return T.querySelector(":has(*,:jqfake)"), !1 } catch (e) { return !0 } }), le.getById ? (b.filter.ID = function (e) { var t = e.replace(O, P); return function (e) { return e.getAttribute("id") === t } }, b.find.ID = function (e, t) { if ("undefined" != typeof t.getElementById && C) { var n = t.getElementById(e); return n ? [n] : [] } }) : (b.filter.ID = function (e) { var n = e.replace(O, P); return function (e) { var t = "undefined" != typeof e.getAttributeNode && e.getAttributeNode("id"); return t && t.value === n } }, b.find.ID = function (e, t) { if ("undefined" != typeof t.getElementById && C) { var n, r, i, o = t.getElementById(e); if (o) { if ((n = o.getAttributeNode("id")) && n.value === e) return [o]; i = t.getElementsByName(e), r = 0; while (o = i[r++]) if ((n = o.getAttributeNode("id")) && n.value === e) return [o] } return [] } }), b.find.TAG = function (e, t) { return "undefined" != typeof t.getElementsByTagName ? t.getElementsByTagName(e) : t.querySelectorAll(e) }, b.find.CLASS = function (e, t) { if ("undefined" != typeof t.getElementsByClassName && C) return t.getElementsByClassName(e) }, d = [], $(function (e) { var t; r.appendChild(e).innerHTML = "", e.querySelectorAll("[selected]").length || d.push("\\[" + ge + "*(?:value|" + f + ")"), e.querySelectorAll("[id~=" + S + "-]").length || d.push("~="), e.querySelectorAll("a#" + S + "+*").length || d.push(".#.+[+~]"), e.querySelectorAll(":checked").length || d.push(":checked"), (t = T.createElement("input")).setAttribute("type", "hidden"), e.appendChild(t).setAttribute("name", "D"), r.appendChild(e).disabled = !0, 2 !== e.querySelectorAll(":disabled").length && d.push(":enabled", ":disabled"), (t = T.createElement("input")).setAttribute("name", ""), e.appendChild(t), e.querySelectorAll("[name='']").length || d.push("\\[" + ge + "*name" + ge + "*=" + ge + "*(?:''|\"\")") }), le.cssHas || d.push(":has"), d = d.length && new RegExp(d.join("|")), l = function (e, t) { if (e === t) return a = !0, 0; var n = !e.compareDocumentPosition - !t.compareDocumentPosition; return n || (1 & (n = (e.ownerDocument || e) == (t.ownerDocument || t) ? e.compareDocumentPosition(t) : 1) || !le.sortDetached && t.compareDocumentPosition(e) === n ? e === T || e.ownerDocument == ye && I.contains(ye, e) ? -1 : t === T || t.ownerDocument == ye && I.contains(ye, t) ? 1 : o ? se.call(o, e) - se.call(o, t) : 0 : 4 & n ? -1 : 1) }), T } for (e in I.matches = function (e, t) { return I(e, null, null, t) }, I.matchesSelector = function (e, t) { if (V(e), C && !h[t + " "] && (!d || !d.test(t))) try { var n = i.call(e, t); if (n || le.disconnectedMatch || e.document && 11 !== e.document.nodeType) return n } catch (e) { h(t, !0) } return 0 < I(t, T, null, [e]).length }, I.contains = function (e, t) { return (e.ownerDocument || e) != T && V(e), ce.contains(e, t) }, I.attr = function (e, t) { (e.ownerDocument || e) != T && V(e); var n = b.attrHandle[t.toLowerCase()], r = n && ue.call(b.attrHandle, t.toLowerCase()) ? n(e, t, !C) : void 0; return void 0 !== r ? r : e.getAttribute(t) }, I.error = function (e) { throw new Error("Syntax error, unrecognized expression: " + e) }, ce.uniqueSort = function (e) { var t, n = [], r = 0, i = 0; if (a = !le.sortStable, o = !le.sortStable && ae.call(e, 0), de.call(e, l), a) { while (t = e[i++]) t === e[i] && (r = n.push(i)); while (r--) he.call(e, n[r], 1) } return o = null, e }, ce.fn.uniqueSort = function () { return this.pushStack(ce.uniqueSort(ae.apply(this))) }, (b = ce.expr = { cacheLength: 50, createPseudo: F, match: D, attrHandle: {}, find: {}, relative: { ">": { dir: "parentNode", first: !0 }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: !0 }, "~": { dir: "previousSibling" } }, preFilter: { ATTR: function (e) { return e[1] = e[1].replace(O, P), e[3] = (e[3] || e[4] || e[5] || "").replace(O, P), "~=" === e[2] && (e[3] = " " + e[3] + " "), e.slice(0, 4) }, CHILD: function (e) { return e[1] = e[1].toLowerCase(), "nth" === e[1].slice(0, 3) ? (e[3] || I.error(e[0]), e[4] = +(e[4] ? e[5] + (e[6] || 1) : 2 * ("even" === e[3] || "odd" === e[3])), e[5] = +(e[7] + e[8] || "odd" === e[3])) : e[3] && I.error(e[0]), e }, PSEUDO: function (e) { var t, n = !e[6] && e[2]; return D.CHILD.test(e[0]) ? null : (e[3] ? e[2] = e[4] || e[5] || "" : n && j.test(n) && (t = Y(n, !0)) && (t = n.indexOf(")", n.length - t) - n.length) && (e[0] = e[0].slice(0, t), e[2] = n.slice(0, t)), e.slice(0, 3)) } }, filter: { TAG: function (e) { var t = e.replace(O, P).toLowerCase(); return "*" === e ? function () { return !0 } : function (e) { return fe(e, t) } }, CLASS: function (e) { var t = s[e + " "]; return t || (t = new RegExp("(^|" + ge + ")" + e + "(" + ge + "|$)")) && s(e, function (e) { return t.test("string" == typeof e.className && e.className || "undefined" != typeof e.getAttribute && e.getAttribute("class") || "") }) }, ATTR: function (n, r, i) { return function (e) { var t = I.attr(e, n); return null == t ? "!=" === r : !r || (t += "", "=" === r ? t === i : "!=" === r ? t !== i : "^=" === r ? i && 0 === t.indexOf(i) : "*=" === r ? i && -1 < t.indexOf(i) : "$=" === r ? i && t.slice(-i.length) === i : "~=" === r ? -1 < (" " + t.replace(v, " ") + " ").indexOf(i) : "|=" === r && (t === i || t.slice(0, i.length + 1) === i + "-")) } }, CHILD: function (d, e, t, h, g) { var v = "nth" !== d.slice(0, 3), y = "last" !== d.slice(-4), m = "of-type" === e; return 1 === h && 0 === g ? function (e) { return !!e.parentNode } : function (e, t, n) { var r, i, o, a, s, u = v !== y ? "nextSibling" : "previousSibling", l = e.parentNode, c = m && e.nodeName.toLowerCase(), f = !n && !m, p = !1; if (l) { if (v) { while (u) { o = e; while (o = o[u]) if (m ? fe(o, c) : 1 === o.nodeType) return !1; s = u = "only" === d && !s && "nextSibling" } return !0 } if (s = [y ? l.firstChild : l.lastChild], y && f) { p = (a = (r = (i = l[S] || (l[S] = {}))[d] || [])[0] === E && r[1]) && r[2], o = a && l.childNodes[a]; while (o = ++a && o && o[u] || (p = a = 0) || s.pop()) if (1 === o.nodeType && ++p && o === e) { i[d] = [E, a, p]; break } } else if (f && (p = a = (r = (i = e[S] || (e[S] = {}))[d] || [])[0] === E && r[1]), !1 === p) while (o = ++a && o && o[u] || (p = a = 0) || s.pop()) if ((m ? fe(o, c) : 1 === o.nodeType) && ++p && (f && ((i = o[S] || (o[S] = {}))[d] = [E, p]), o === e)) break; return (p -= g) === h || p % h == 0 && 0 <= p / h } } }, PSEUDO: function (e, o) { var t, a = b.pseudos[e] || b.setFilters[e.toLowerCase()] || I.error("unsupported pseudo: " + e); return a[S] ? a(o) : 1 < a.length ? (t = [e, e, "", o], b.setFilters.hasOwnProperty(e.toLowerCase()) ? F(function (e, t) { var n, r = a(e, o), i = r.length; while (i--) e[n = se.call(e, r[i])] = !(t[n] = r[i]) }) : function (e) { return a(e, 0, t) }) : a } }, pseudos: { not: F(function (e) { var r = [], i = [], s = ne(e.replace(ve, "$1")); return s[S] ? F(function (e, t, n, r) { var i, o = s(e, null, r, []), a = e.length; while (a--) (i = o[a]) && (e[a] = !(t[a] = i)) }) : function (e, t, n) { return r[0] = e, s(r, null, n, i), r[0] = null, !i.pop() } }), has: F(function (t) { return function (e) { return 0 < I(t, e).length } }), contains: F(function (t) { return t = t.replace(O, P), function (e) { return -1 < (e.textContent || ce.text(e)).indexOf(t) } }), lang: F(function (n) { return A.test(n || "") || I.error("unsupported lang: " + n), n = n.replace(O, P).toLowerCase(), function (e) { var t; do { if (t = C ? e.lang : e.getAttribute("xml:lang") || e.getAttribute("lang")) return (t = t.toLowerCase()) === n || 0 === t.indexOf(n + "-") } while ((e = e.parentNode) && 1 === e.nodeType); return !1 } }), target: function (e) { var t = ie.location && ie.location.hash; return t && t.slice(1) === e.id }, root: function (e) { return e === r }, focus: function (e) { return e === function () { try { return T.activeElement } catch (e) { } }() && T.hasFocus() && !!(e.type || e.href || ~e.tabIndex) }, enabled: X(!1), disabled: X(!0), checked: function (e) { return fe(e, "input") && !!e.checked || fe(e, "option") && !!e.selected }, selected: function (e) { return e.parentNode && e.parentNode.selectedIndex, !0 === e.selected }, empty: function (e) { for (e = e.firstChild; e; e = e.nextSibling)if (e.nodeType < 6) return !1; return !0 }, parent: function (e) { return !b.pseudos.empty(e) }, header: function (e) { return q.test(e.nodeName) }, input: function (e) { return N.test(e.nodeName) }, button: function (e) { return fe(e, "input") && "button" === e.type || fe(e, "button") }, text: function (e) { var t; return fe(e, "input") && "text" === e.type && (null == (t = e.getAttribute("type")) || "text" === t.toLowerCase()) }, first: U(function () { return [0] }), last: U(function (e, t) { return [t - 1] }), eq: U(function (e, t, n) { return [n < 0 ? n + t : n] }), even: U(function (e, t) { for (var n = 0; n < t; n += 2)e.push(n); return e }), odd: U(function (e, t) { for (var n = 1; n < t; n += 2)e.push(n); return e }), lt: U(function (e, t, n) { var r; for (r = n < 0 ? n + t : t < n ? t : n; 0 <= --r;)e.push(r); return e }), gt: U(function (e, t, n) { for (var r = n < 0 ? n + t : n; ++r < t;)e.push(r); return e }) } }).pseudos.nth = b.pseudos.eq, { radio: !0, checkbox: !0, file: !0, password: !0, image: !0 }) b.pseudos[e] = B(e); for (e in { submit: !0, reset: !0 }) b.pseudos[e] = _(e); function G() { } function Y(e, t) { var n, r, i, o, a, s, u, l = c[e + " "]; if (l) return t ? 0 : l.slice(0); a = e, s = [], u = b.preFilter; while (a) { for (o in n && !(r = y.exec(a)) || (r && (a = a.slice(r[0].length) || a), s.push(i = [])), n = !1, (r = m.exec(a)) && (n = r.shift(), i.push({ value: n, type: r[0].replace(ve, " ") }), a = a.slice(n.length)), b.filter) !(r = D[o].exec(a)) || u[o] && !(r = u[o](r)) || (n = r.shift(), i.push({ value: n, type: o, matches: r }), a = a.slice(n.length)); if (!n) break } return t ? a.length : a ? I.error(e) : c(e, s).slice(0) } function Q(e) { for (var t = 0, n = e.length, r = ""; t < n; t++)r += e[t].value; return r } function J(a, e, t) { var s = e.dir, u = e.next, l = u || s, c = t && "parentNode" === l, f = n++; return e.first ? function (e, t, n) { while (e = e[s]) if (1 === e.nodeType || c) return a(e, t, n); return !1 } : function (e, t, n) { var r, i, o = [E, f]; if (n) { while (e = e[s]) if ((1 === e.nodeType || c) && a(e, t, n)) return !0 } else while (e = e[s]) if (1 === e.nodeType || c) if (i = e[S] || (e[S] = {}), u && fe(e, u)) e = e[s] || e; else { if ((r = i[l]) && r[0] === E && r[1] === f) return o[2] = r[2]; if ((i[l] = o)[2] = a(e, t, n)) return !0 } return !1 } } function K(i) { return 1 < i.length ? function (e, t, n) { var r = i.length; while (r--) if (!i[r](e, t, n)) return !1; return !0 } : i[0] } function Z(e, t, n, r, i) { for (var o, a = [], s = 0, u = e.length, l = null != t; s < u; s++)(o = e[s]) && (n && !n(o, r, i) || (a.push(o), l && t.push(s))); return a } function ee(d, h, g, v, y, e) { return v && !v[S] && (v = ee(v)), y && !y[S] && (y = ee(y, e)), F(function (e, t, n, r) { var i, o, a, s, u = [], l = [], c = t.length, f = e || function (e, t, n) { for (var r = 0, i = t.length; r < i; r++)I(e, t[r], n); return n }(h || "*", n.nodeType ? [n] : n, []), p = !d || !e && h ? f : Z(f, u, d, n, r); if (g ? g(p, s = y || (e ? d : c || v) ? [] : t, n, r) : s = p, v) { i = Z(s, l), v(i, [], n, r), o = i.length; while (o--) (a = i[o]) && (s[l[o]] = !(p[l[o]] = a)) } if (e) { if (y || d) { if (y) { i = [], o = s.length; while (o--) (a = s[o]) && i.push(p[o] = a); y(null, s = [], i, r) } o = s.length; while (o--) (a = s[o]) && -1 < (i = y ? se.call(e, a) : u[o]) && (e[i] = !(t[i] = a)) } } else s = Z(s === t ? s.splice(c, s.length) : s), y ? y(null, t, s, r) : k.apply(t, s) }) } function te(e) { for (var i, t, n, r = e.length, o = b.relative[e[0].type], a = o || b.relative[" "], s = o ? 1 : 0, u = J(function (e) { return e === i }, a, !0), l = J(function (e) { return -1 < se.call(i, e) }, a, !0), c = [function (e, t, n) { var r = !o && (n || t != w) || ((i = t).nodeType ? u(e, t, n) : l(e, t, n)); return i = null, r }]; s < r; s++)if (t = b.relative[e[s].type]) c = [J(K(c), t)]; else { if ((t = b.filter[e[s].type].apply(null, e[s].matches))[S]) { for (n = ++s; n < r; n++)if (b.relative[e[n].type]) break; return ee(1 < s && K(c), 1 < s && Q(e.slice(0, s - 1).concat({ value: " " === e[s - 2].type ? "*" : "" })).replace(ve, "$1"), t, s < n && te(e.slice(s, n)), n < r && te(e = e.slice(n)), n < r && Q(e)) } c.push(t) } return K(c) } function ne(e, t) { var n, v, y, m, x, r, i = [], o = [], a = u[e + " "]; if (!a) { t || (t = Y(e)), n = t.length; while (n--) (a = te(t[n]))[S] ? i.push(a) : o.push(a); (a = u(e, (v = o, m = 0 < (y = i).length, x = 0 < v.length, r = function (e, t, n, r, i) { var o, a, s, u = 0, l = "0", c = e && [], f = [], p = w, d = e || x && b.find.TAG("*", i), h = E += null == p ? 1 : Math.random() || .1, g = d.length; for (i && (w = t == T || t || i); l !== g && null != (o = d[l]); l++) { if (x && o) { a = 0, t || o.ownerDocument == T || (V(o), n = !C); while (s = v[a++]) if (s(o, t || T, n)) { k.call(r, o); break } i && (E = h) } m && ((o = !s && o) && u--, e && c.push(o)) } if (u += l, m && l !== u) { a = 0; while (s = y[a++]) s(c, f, t, n); if (e) { if (0 < u) while (l--) c[l] || f[l] || (f[l] = pe.call(r)); f = Z(f) } k.apply(r, f), i && !e && 0 < f.length && 1 < u + y.length && ce.uniqueSort(r) } return i && (E = h, w = p), c }, m ? F(r) : r))).selector = e } return a } function re(e, t, n, r) { var i, o, a, s, u, l = "function" == typeof e && e, c = !r && Y(e = l.selector || e); if (n = n || [], 1 === c.length) { if (2 < (o = c[0] = c[0].slice(0)).length && "ID" === (a = o[0]).type && 9 === t.nodeType && C && b.relative[o[1].type]) { if (!(t = (b.find.ID(a.matches[0].replace(O, P), t) || [])[0])) return n; l && (t = t.parentNode), e = e.slice(o.shift().value.length) } i = D.needsContext.test(e) ? 0 : o.length; while (i--) { if (a = o[i], b.relative[s = a.type]) break; if ((u = b.find[s]) && (r = u(a.matches[0].replace(O, P), H.test(o[0].type) && z(t.parentNode) || t))) { if (o.splice(i, 1), !(e = r.length && Q(o))) return k.apply(n, r), n; break } } } return (l || ne(e, c))(r, t, !C, n, !t || H.test(e) && z(t.parentNode) || t), n } G.prototype = b.filters = b.pseudos, b.setFilters = new G, le.sortStable = S.split("").sort(l).join("") === S, V(), le.sortDetached = $(function (e) { return 1 & e.compareDocumentPosition(T.createElement("fieldset")) }), ce.find = I, ce.expr[":"] = ce.expr.pseudos, ce.unique = ce.uniqueSort, I.compile = ne, I.select = re, I.setDocument = V, I.escape = ce.escapeSelector, I.getText = ce.text, I.isXML = ce.isXMLDoc, I.selectors = ce.expr, I.support = ce.support, I.uniqueSort = ce.uniqueSort }(); var d = function (e, t, n) { var r = [], i = void 0 !== n; while ((e = e[t]) && 9 !== e.nodeType) if (1 === e.nodeType) { if (i && ce(e).is(n)) break; r.push(e) } return r }, h = function (e, t) { for (var n = []; e; e = e.nextSibling)1 === e.nodeType && e !== t && n.push(e); return n }, b = ce.expr.match.needsContext, w = /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i; function T(e, n, r) { return v(n) ? ce.grep(e, function (e, t) { return !!n.call(e, t, e) !== r }) : n.nodeType ? ce.grep(e, function (e) { return e === n !== r }) : "string" != typeof n ? ce.grep(e, function (e) { return -1 < se.call(n, e) !== r }) : ce.filter(n, e, r) } ce.filter = function (e, t, n) { var r = t[0]; return n && (e = ":not(" + e + ")"), 1 === t.length && 1 === r.nodeType ? ce.find.matchesSelector(r, e) ? [r] : [] : ce.find.matches(e, ce.grep(t, function (e) { return 1 === e.nodeType })) }, ce.fn.extend({ find: function (e) { var t, n, r = this.length, i = this; if ("string" != typeof e) return this.pushStack(ce(e).filter(function () { for (t = 0; t < r; t++)if (ce.contains(i[t], this)) return !0 })); for (n = this.pushStack([]), t = 0; t < r; t++)ce.find(e, i[t], n); return 1 < r ? ce.uniqueSort(n) : n }, filter: function (e) { return this.pushStack(T(this, e || [], !1)) }, not: function (e) { return this.pushStack(T(this, e || [], !0)) }, is: function (e) { return !!T(this, "string" == typeof e && b.test(e) ? ce(e) : e || [], !1).length } }); var k, S = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/; (ce.fn.init = function (e, t, n) { var r, i; if (!e) return this; if (n = n || k, "string" == typeof e) { if (!(r = "<" === e[0] && ">" === e[e.length - 1] && 3 <= e.length ? [null, e, null] : S.exec(e)) || !r[1] && t) return !t || t.jquery ? (t || n).find(e) : this.constructor(t).find(e); if (r[1]) { if (t = t instanceof ce ? t[0] : t, ce.merge(this, ce.parseHTML(r[1], t && t.nodeType ? t.ownerDocument || t : C, !0)), w.test(r[1]) && ce.isPlainObject(t)) for (r in t) v(this[r]) ? this[r](t[r]) : this.attr(r, t[r]); return this } return (i = C.getElementById(r[2])) && (this[0] = i, this.length = 1), this } return e.nodeType ? (this[0] = e, this.length = 1, this) : v(e) ? void 0 !== n.ready ? n.ready(e) : e(ce) : ce.makeArray(e, this) }).prototype = ce.fn, k = ce(C); var E = /^(?:parents|prev(?:Until|All))/, j = { children: !0, contents: !0, next: !0, prev: !0 }; function A(e, t) { while ((e = e[t]) && 1 !== e.nodeType); return e } ce.fn.extend({ has: function (e) { var t = ce(e, this), n = t.length; return this.filter(function () { for (var e = 0; e < n; e++)if (ce.contains(this, t[e])) return !0 }) }, closest: function (e, t) { var n, r = 0, i = this.length, o = [], a = "string" != typeof e && ce(e); if (!b.test(e)) for (; r < i; r++)for (n = this[r]; n && n !== t; n = n.parentNode)if (n.nodeType < 11 && (a ? -1 < a.index(n) : 1 === n.nodeType && ce.find.matchesSelector(n, e))) { o.push(n); break } return this.pushStack(1 < o.length ? ce.uniqueSort(o) : o) }, index: function (e) { return e ? "string" == typeof e ? se.call(ce(e), this[0]) : se.call(this, e.jquery ? e[0] : e) : this[0] && this[0].parentNode ? this.first().prevAll().length : -1 }, add: function (e, t) { return this.pushStack(ce.uniqueSort(ce.merge(this.get(), ce(e, t)))) }, addBack: function (e) { return this.add(null == e ? this.prevObject : this.prevObject.filter(e)) } }), ce.each({ parent: function (e) { var t = e.parentNode; return t && 11 !== t.nodeType ? t : null }, parents: function (e) { return d(e, "parentNode") }, parentsUntil: function (e, t, n) { return d(e, "parentNode", n) }, next: function (e) { return A(e, "nextSibling") }, prev: function (e) { return A(e, "previousSibling") }, nextAll: function (e) { return d(e, "nextSibling") }, prevAll: function (e) { return d(e, "previousSibling") }, nextUntil: function (e, t, n) { return d(e, "nextSibling", n) }, prevUntil: function (e, t, n) { return d(e, "previousSibling", n) }, siblings: function (e) { return h((e.parentNode || {}).firstChild, e) }, children: function (e) { return h(e.firstChild) }, contents: function (e) { return null != e.contentDocument && r(e.contentDocument) ? e.contentDocument : (fe(e, "template") && (e = e.content || e), ce.merge([], e.childNodes)) } }, function (r, i) { ce.fn[r] = function (e, t) { var n = ce.map(this, i, e); return "Until" !== r.slice(-5) && (t = e), t && "string" == typeof t && (n = ce.filter(t, n)), 1 < this.length && (j[r] || ce.uniqueSort(n), E.test(r) && n.reverse()), this.pushStack(n) } }); var D = /[^\x20\t\r\n\f]+/g; function N(e) { return e } function q(e) { throw e } function L(e, t, n, r) { var i; try { e && v(i = e.promise) ? i.call(e).done(t).fail(n) : e && v(i = e.then) ? i.call(e, t, n) : t.apply(void 0, [e].slice(r)) } catch (e) { n.apply(void 0, [e]) } } ce.Callbacks = function (r) { var e, n; r = "string" == typeof r ? (e = r, n = {}, ce.each(e.match(D) || [], function (e, t) { n[t] = !0 }), n) : ce.extend({}, r); var i, t, o, a, s = [], u = [], l = -1, c = function () { for (a = a || r.once, o = i = !0; u.length; l = -1) { t = u.shift(); while (++l < s.length) !1 === s[l].apply(t[0], t[1]) && r.stopOnFalse && (l = s.length, t = !1) } r.memory || (t = !1), i = !1, a && (s = t ? [] : "") }, f = { add: function () { return s && (t && !i && (l = s.length - 1, u.push(t)), function n(e) { ce.each(e, function (e, t) { v(t) ? r.unique && f.has(t) || s.push(t) : t && t.length && "string" !== x(t) && n(t) }) }(arguments), t && !i && c()), this }, remove: function () { return ce.each(arguments, function (e, t) { var n; while (-1 < (n = ce.inArray(t, s, n))) s.splice(n, 1), n <= l && l-- }), this }, has: function (e) { return e ? -1 < ce.inArray(e, s) : 0 < s.length }, empty: function () { return s && (s = []), this }, disable: function () { return a = u = [], s = t = "", this }, disabled: function () { return !s }, lock: function () { return a = u = [], t || i || (s = t = ""), this }, locked: function () { return !!a }, fireWith: function (e, t) { return a || (t = [e, (t = t || []).slice ? t.slice() : t], u.push(t), i || c()), this }, fire: function () { return f.fireWith(this, arguments), this }, fired: function () { return !!o } }; return f }, ce.extend({ Deferred: function (e) { var o = [["notify", "progress", ce.Callbacks("memory"), ce.Callbacks("memory"), 2], ["resolve", "done", ce.Callbacks("once memory"), ce.Callbacks("once memory"), 0, "resolved"], ["reject", "fail", ce.Callbacks("once memory"), ce.Callbacks("once memory"), 1, "rejected"]], i = "pending", a = { state: function () { return i }, always: function () { return s.done(arguments).fail(arguments), this }, "catch": function (e) { return a.then(null, e) }, pipe: function () { var i = arguments; return ce.Deferred(function (r) { ce.each(o, function (e, t) { var n = v(i[t[4]]) && i[t[4]]; s[t[1]](function () { var e = n && n.apply(this, arguments); e && v(e.promise) ? e.promise().progress(r.notify).done(r.resolve).fail(r.reject) : r[t[0] + "With"](this, n ? [e] : arguments) }) }), i = null }).promise() }, then: function (t, n, r) { var u = 0; function l(i, o, a, s) { return function () { var n = this, r = arguments, e = function () { var e, t; if (!(i < u)) { if ((e = a.apply(n, r)) === o.promise()) throw new TypeError("Thenable self-resolution"); t = e && ("object" == typeof e || "function" == typeof e) && e.then, v(t) ? s ? t.call(e, l(u, o, N, s), l(u, o, q, s)) : (u++, t.call(e, l(u, o, N, s), l(u, o, q, s), l(u, o, N, o.notifyWith))) : (a !== N && (n = void 0, r = [e]), (s || o.resolveWith)(n, r)) } }, t = s ? e : function () { try { e() } catch (e) { ce.Deferred.exceptionHook && ce.Deferred.exceptionHook(e, t.error), u <= i + 1 && (a !== q && (n = void 0, r = [e]), o.rejectWith(n, r)) } }; i ? t() : (ce.Deferred.getErrorHook ? t.error = ce.Deferred.getErrorHook() : ce.Deferred.getStackHook && (t.error = ce.Deferred.getStackHook()), ie.setTimeout(t)) } } return ce.Deferred(function (e) { o[0][3].add(l(0, e, v(r) ? r : N, e.notifyWith)), o[1][3].add(l(0, e, v(t) ? t : N)), o[2][3].add(l(0, e, v(n) ? n : q)) }).promise() }, promise: function (e) { return null != e ? ce.extend(e, a) : a } }, s = {}; return ce.each(o, function (e, t) { var n = t[2], r = t[5]; a[t[1]] = n.add, r && n.add(function () { i = r }, o[3 - e][2].disable, o[3 - e][3].disable, o[0][2].lock, o[0][3].lock), n.add(t[3].fire), s[t[0]] = function () { return s[t[0] + "With"](this === s ? void 0 : this, arguments), this }, s[t[0] + "With"] = n.fireWith }), a.promise(s), e && e.call(s, s), s }, when: function (e) { var n = arguments.length, t = n, r = Array(t), i = ae.call(arguments), o = ce.Deferred(), a = function (t) { return function (e) { r[t] = this, i[t] = 1 < arguments.length ? ae.call(arguments) : e, --n || o.resolveWith(r, i) } }; if (n <= 1 && (L(e, o.done(a(t)).resolve, o.reject, !n), "pending" === o.state() || v(i[t] && i[t].then))) return o.then(); while (t--) L(i[t], a(t), o.reject); return o.promise() } }); var H = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; ce.Deferred.exceptionHook = function (e, t) { ie.console && ie.console.warn && e && H.test(e.name) && ie.console.warn("jQuery.Deferred exception: " + e.message, e.stack, t) }, ce.readyException = function (e) { ie.setTimeout(function () { throw e }) }; var O = ce.Deferred(); function P() { C.removeEventListener("DOMContentLoaded", P), ie.removeEventListener("load", P), ce.ready() } ce.fn.ready = function (e) { return O.then(e)["catch"](function (e) { ce.readyException(e) }), this }, ce.extend({ isReady: !1, readyWait: 1, ready: function (e) { (!0 === e ? --ce.readyWait : ce.isReady) || (ce.isReady = !0) !== e && 0 < --ce.readyWait || O.resolveWith(C, [ce]) } }), ce.ready.then = O.then, "complete" === C.readyState || "loading" !== C.readyState && !C.documentElement.doScroll ? ie.setTimeout(ce.ready) : (C.addEventListener("DOMContentLoaded", P), ie.addEventListener("load", P)); var R = function (e, t, n, r, i, o, a) { var s = 0, u = e.length, l = null == n; if ("object" === x(n)) for (s in i = !0, n) R(e, t, s, n[s], !0, o, a); else if (void 0 !== r && (i = !0, v(r) || (a = !0), l && (a ? (t.call(e, r), t = null) : (l = t, t = function (e, t, n) { return l.call(ce(e), n) })), t)) for (; s < u; s++)t(e[s], n, a ? r : r.call(e[s], s, t(e[s], n))); return i ? e : l ? t.call(e) : u ? t(e[0], n) : o }, M = /^-ms-/, I = /-([a-z])/g; function W(e, t) { return t.toUpperCase() } function F(e) { return e.replace(M, "ms-").replace(I, W) } var $ = function (e) { return 1 === e.nodeType || 9 === e.nodeType || !+e.nodeType }; function B() { this.expando = ce.expando + B.uid++ } B.uid = 1, B.prototype = { cache: function (e) { var t = e[this.expando]; return t || (t = {}, $(e) && (e.nodeType ? e[this.expando] = t : Object.defineProperty(e, this.expando, { value: t, configurable: !0 }))), t }, set: function (e, t, n) { var r, i = this.cache(e); if ("string" == typeof t) i[F(t)] = n; else for (r in t) i[F(r)] = t[r]; return i }, get: function (e, t) { return void 0 === t ? this.cache(e) : e[this.expando] && e[this.expando][F(t)] }, access: function (e, t, n) { return void 0 === t || t && "string" == typeof t && void 0 === n ? this.get(e, t) : (this.set(e, t, n), void 0 !== n ? n : t) }, remove: function (e, t) { var n, r = e[this.expando]; if (void 0 !== r) { if (void 0 !== t) { n = (t = Array.isArray(t) ? t.map(F) : (t = F(t)) in r ? [t] : t.match(D) || []).length; while (n--) delete r[t[n]] } (void 0 === t || ce.isEmptyObject(r)) && (e.nodeType ? e[this.expando] = void 0 : delete e[this.expando]) } }, hasData: function (e) { var t = e[this.expando]; return void 0 !== t && !ce.isEmptyObject(t) } }; var _ = new B, X = new B, U = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, z = /[A-Z]/g; function V(e, t, n) { var r, i; if (void 0 === n && 1 === e.nodeType) if (r = "data-" + t.replace(z, "-$&").toLowerCase(), "string" == typeof (n = e.getAttribute(r))) { try { n = "true" === (i = n) || "false" !== i && ("null" === i ? null : i === +i + "" ? +i : U.test(i) ? JSON.parse(i) : i) } catch (e) { } X.set(e, t, n) } else n = void 0; return n } ce.extend({ hasData: function (e) { return X.hasData(e) || _.hasData(e) }, data: function (e, t, n) { return X.access(e, t, n) }, removeData: function (e, t) { X.remove(e, t) }, _data: function (e, t, n) { return _.access(e, t, n) }, _removeData: function (e, t) { _.remove(e, t) } }), ce.fn.extend({ data: function (n, e) { var t, r, i, o = this[0], a = o && o.attributes; if (void 0 === n) { if (this.length && (i = X.get(o), 1 === o.nodeType && !_.get(o, "hasDataAttrs"))) { t = a.length; while (t--) a[t] && 0 === (r = a[t].name).indexOf("data-") && (r = F(r.slice(5)), V(o, r, i[r])); _.set(o, "hasDataAttrs", !0) } return i } return "object" == typeof n ? this.each(function () { X.set(this, n) }) : R(this, function (e) { var t; if (o && void 0 === e) return void 0 !== (t = X.get(o, n)) ? t : void 0 !== (t = V(o, n)) ? t : void 0; this.each(function () { X.set(this, n, e) }) }, null, e, 1 < arguments.length, null, !0) }, removeData: function (e) { return this.each(function () { X.remove(this, e) }) } }), ce.extend({ queue: function (e, t, n) { var r; if (e) return t = (t || "fx") + "queue", r = _.get(e, t), n && (!r || Array.isArray(n) ? r = _.access(e, t, ce.makeArray(n)) : r.push(n)), r || [] }, dequeue: function (e, t) { t = t || "fx"; var n = ce.queue(e, t), r = n.length, i = n.shift(), o = ce._queueHooks(e, t); "inprogress" === i && (i = n.shift(), r--), i && ("fx" === t && n.unshift("inprogress"), delete o.stop, i.call(e, function () { ce.dequeue(e, t) }, o)), !r && o && o.empty.fire() }, _queueHooks: function (e, t) { var n = t + "queueHooks"; return _.get(e, n) || _.access(e, n, { empty: ce.Callbacks("once memory").add(function () { _.remove(e, [t + "queue", n]) }) }) } }), ce.fn.extend({ queue: function (t, n) { var e = 2; return "string" != typeof t && (n = t, t = "fx", e--), arguments.length < e ? ce.queue(this[0], t) : void 0 === n ? this : this.each(function () { var e = ce.queue(this, t, n); ce._queueHooks(this, t), "fx" === t && "inprogress" !== e[0] && ce.dequeue(this, t) }) }, dequeue: function (e) { return this.each(function () { ce.dequeue(this, e) }) }, clearQueue: function (e) { return this.queue(e || "fx", []) }, promise: function (e, t) { var n, r = 1, i = ce.Deferred(), o = this, a = this.length, s = function () { --r || i.resolveWith(o, [o]) }; "string" != typeof e && (t = e, e = void 0), e = e || "fx"; while (a--) (n = _.get(o[a], e + "queueHooks")) && n.empty && (r++, n.empty.add(s)); return s(), i.promise(t) } }); var G = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, Y = new RegExp("^(?:([+-])=|)(" + G + ")([a-z%]*)$", "i"), Q = ["Top", "Right", "Bottom", "Left"], J = C.documentElement, K = function (e) { return ce.contains(e.ownerDocument, e) }, Z = { composed: !0 }; J.getRootNode && (K = function (e) { return ce.contains(e.ownerDocument, e) || e.getRootNode(Z) === e.ownerDocument }); var ee = function (e, t) { return "none" === (e = t || e).style.display || "" === e.style.display && K(e) && "none" === ce.css(e, "display") }; function te(e, t, n, r) { var i, o, a = 20, s = r ? function () { return r.cur() } : function () { return ce.css(e, t, "") }, u = s(), l = n && n[3] || (ce.cssNumber[t] ? "" : "px"), c = e.nodeType && (ce.cssNumber[t] || "px" !== l && +u) && Y.exec(ce.css(e, t)); if (c && c[3] !== l) { u /= 2, l = l || c[3], c = +u || 1; while (a--) ce.style(e, t, c + l), (1 - o) * (1 - (o = s() / u || .5)) <= 0 && (a = 0), c /= o; c *= 2, ce.style(e, t, c + l), n = n || [] } return n && (c = +c || +u || 0, i = n[1] ? c + (n[1] + 1) * n[2] : +n[2], r && (r.unit = l, r.start = c, r.end = i)), i } var ne = {}; function re(e, t) { for (var n, r, i, o, a, s, u, l = [], c = 0, f = e.length; c < f; c++)(r = e[c]).style && (n = r.style.display, t ? ("none" === n && (l[c] = _.get(r, "display") || null, l[c] || (r.style.display = "")), "" === r.style.display && ee(r) && (l[c] = (u = a = o = void 0, a = (i = r).ownerDocument, s = i.nodeName, (u = ne[s]) || (o = a.body.appendChild(a.createElement(s)), u = ce.css(o, "display"), o.parentNode.removeChild(o), "none" === u && (u = "block"), ne[s] = u)))) : "none" !== n && (l[c] = "none", _.set(r, "display", n))); for (c = 0; c < f; c++)null != l[c] && (e[c].style.display = l[c]); return e } ce.fn.extend({ show: function () { return re(this, !0) }, hide: function () { return re(this) }, toggle: function (e) { return "boolean" == typeof e ? e ? this.show() : this.hide() : this.each(function () { ee(this) ? ce(this).show() : ce(this).hide() }) } }); var xe, be, we = /^(?:checkbox|radio)$/i, Te = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i, Ce = /^$|^module$|\/(?:java|ecma)script/i; xe = C.createDocumentFragment().appendChild(C.createElement("div")), (be = C.createElement("input")).setAttribute("type", "radio"), be.setAttribute("checked", "checked"), be.setAttribute("name", "t"), xe.appendChild(be), le.checkClone = xe.cloneNode(!0).cloneNode(!0).lastChild.checked, xe.innerHTML = "", le.noCloneChecked = !!xe.cloneNode(!0).lastChild.defaultValue, xe.innerHTML = "", le.option = !!xe.lastChild; var ke = { thead: [1, "", "
"], col: [2, "", "
"], tr: [2, "", "
"], td: [3, "", "
"], _default: [0, "", ""] }; function Se(e, t) { var n; return n = "undefined" != typeof e.getElementsByTagName ? e.getElementsByTagName(t || "*") : "undefined" != typeof e.querySelectorAll ? e.querySelectorAll(t || "*") : [], void 0 === t || t && fe(e, t) ? ce.merge([e], n) : n } function Ee(e, t) { for (var n = 0, r = e.length; n < r; n++)_.set(e[n], "globalEval", !t || _.get(t[n], "globalEval")) } ke.tbody = ke.tfoot = ke.colgroup = ke.caption = ke.thead, ke.th = ke.td, le.option || (ke.optgroup = ke.option = [1, ""]); var je = /<|&#?\w+;/; function Ae(e, t, n, r, i) { for (var o, a, s, u, l, c, f = t.createDocumentFragment(), p = [], d = 0, h = e.length; d < h; d++)if ((o = e[d]) || 0 === o) if ("object" === x(o)) ce.merge(p, o.nodeType ? [o] : o); else if (je.test(o)) { a = a || f.appendChild(t.createElement("div")), s = (Te.exec(o) || ["", ""])[1].toLowerCase(), u = ke[s] || ke._default, a.innerHTML = u[1] + ce.htmlPrefilter(o) + u[2], c = u[0]; while (c--) a = a.lastChild; ce.merge(p, a.childNodes), (a = f.firstChild).textContent = "" } else p.push(t.createTextNode(o)); f.textContent = "", d = 0; while (o = p[d++]) if (r && -1 < ce.inArray(o, r)) i && i.push(o); else if (l = K(o), a = Se(f.appendChild(o), "script"), l && Ee(a), n) { c = 0; while (o = a[c++]) Ce.test(o.type || "") && n.push(o) } return f } var De = /^([^.]*)(?:\.(.+)|)/; function Ne() { return !0 } function qe() { return !1 } function Le(e, t, n, r, i, o) { var a, s; if ("object" == typeof t) { for (s in "string" != typeof n && (r = r || n, n = void 0), t) Le(e, s, n, r, t[s], o); return e } if (null == r && null == i ? (i = n, r = n = void 0) : null == i && ("string" == typeof n ? (i = r, r = void 0) : (i = r, r = n, n = void 0)), !1 === i) i = qe; else if (!i) return e; return 1 === o && (a = i, (i = function (e) { return ce().off(e), a.apply(this, arguments) }).guid = a.guid || (a.guid = ce.guid++)), e.each(function () { ce.event.add(this, t, i, r, n) }) } function He(e, r, t) { t ? (_.set(e, r, !1), ce.event.add(e, r, { namespace: !1, handler: function (e) { var t, n = _.get(this, r); if (1 & e.isTrigger && this[r]) { if (n) (ce.event.special[r] || {}).delegateType && e.stopPropagation(); else if (n = ae.call(arguments), _.set(this, r, n), this[r](), t = _.get(this, r), _.set(this, r, !1), n !== t) return e.stopImmediatePropagation(), e.preventDefault(), t } else n && (_.set(this, r, ce.event.trigger(n[0], n.slice(1), this)), e.stopPropagation(), e.isImmediatePropagationStopped = Ne) } })) : void 0 === _.get(e, r) && ce.event.add(e, r, Ne) } ce.event = { global: {}, add: function (t, e, n, r, i) { var o, a, s, u, l, c, f, p, d, h, g, v = _.get(t); if ($(t)) { n.handler && (n = (o = n).handler, i = o.selector), i && ce.find.matchesSelector(J, i), n.guid || (n.guid = ce.guid++), (u = v.events) || (u = v.events = Object.create(null)), (a = v.handle) || (a = v.handle = function (e) { return "undefined" != typeof ce && ce.event.triggered !== e.type ? ce.event.dispatch.apply(t, arguments) : void 0 }), l = (e = (e || "").match(D) || [""]).length; while (l--) d = g = (s = De.exec(e[l]) || [])[1], h = (s[2] || "").split(".").sort(), d && (f = ce.event.special[d] || {}, d = (i ? f.delegateType : f.bindType) || d, f = ce.event.special[d] || {}, c = ce.extend({ type: d, origType: g, data: r, handler: n, guid: n.guid, selector: i, needsContext: i && ce.expr.match.needsContext.test(i), namespace: h.join(".") }, o), (p = u[d]) || ((p = u[d] = []).delegateCount = 0, f.setup && !1 !== f.setup.call(t, r, h, a) || t.addEventListener && t.addEventListener(d, a)), f.add && (f.add.call(t, c), c.handler.guid || (c.handler.guid = n.guid)), i ? p.splice(p.delegateCount++, 0, c) : p.push(c), ce.event.global[d] = !0) } }, remove: function (e, t, n, r, i) { var o, a, s, u, l, c, f, p, d, h, g, v = _.hasData(e) && _.get(e); if (v && (u = v.events)) { l = (t = (t || "").match(D) || [""]).length; while (l--) if (d = g = (s = De.exec(t[l]) || [])[1], h = (s[2] || "").split(".").sort(), d) { f = ce.event.special[d] || {}, p = u[d = (r ? f.delegateType : f.bindType) || d] || [], s = s[2] && new RegExp("(^|\\.)" + h.join("\\.(?:.*\\.|)") + "(\\.|$)"), a = o = p.length; while (o--) c = p[o], !i && g !== c.origType || n && n.guid !== c.guid || s && !s.test(c.namespace) || r && r !== c.selector && ("**" !== r || !c.selector) || (p.splice(o, 1), c.selector && p.delegateCount--, f.remove && f.remove.call(e, c)); a && !p.length && (f.teardown && !1 !== f.teardown.call(e, h, v.handle) || ce.removeEvent(e, d, v.handle), delete u[d]) } else for (d in u) ce.event.remove(e, d + t[l], n, r, !0); ce.isEmptyObject(u) && _.remove(e, "handle events") } }, dispatch: function (e) { var t, n, r, i, o, a, s = new Array(arguments.length), u = ce.event.fix(e), l = (_.get(this, "events") || Object.create(null))[u.type] || [], c = ce.event.special[u.type] || {}; for (s[0] = u, t = 1; t < arguments.length; t++)s[t] = arguments[t]; if (u.delegateTarget = this, !c.preDispatch || !1 !== c.preDispatch.call(this, u)) { a = ce.event.handlers.call(this, u, l), t = 0; while ((i = a[t++]) && !u.isPropagationStopped()) { u.currentTarget = i.elem, n = 0; while ((o = i.handlers[n++]) && !u.isImmediatePropagationStopped()) u.rnamespace && !1 !== o.namespace && !u.rnamespace.test(o.namespace) || (u.handleObj = o, u.data = o.data, void 0 !== (r = ((ce.event.special[o.origType] || {}).handle || o.handler).apply(i.elem, s)) && !1 === (u.result = r) && (u.preventDefault(), u.stopPropagation())) } return c.postDispatch && c.postDispatch.call(this, u), u.result } }, handlers: function (e, t) { var n, r, i, o, a, s = [], u = t.delegateCount, l = e.target; if (u && l.nodeType && !("click" === e.type && 1 <= e.button)) for (; l !== this; l = l.parentNode || this)if (1 === l.nodeType && ("click" !== e.type || !0 !== l.disabled)) { for (o = [], a = {}, n = 0; n < u; n++)void 0 === a[i = (r = t[n]).selector + " "] && (a[i] = r.needsContext ? -1 < ce(i, this).index(l) : ce.find(i, this, null, [l]).length), a[i] && o.push(r); o.length && s.push({ elem: l, handlers: o }) } return l = this, u < t.length && s.push({ elem: l, handlers: t.slice(u) }), s }, addProp: function (t, e) { Object.defineProperty(ce.Event.prototype, t, { enumerable: !0, configurable: !0, get: v(e) ? function () { if (this.originalEvent) return e(this.originalEvent) } : function () { if (this.originalEvent) return this.originalEvent[t] }, set: function (e) { Object.defineProperty(this, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) } }) }, fix: function (e) { return e[ce.expando] ? e : new ce.Event(e) }, special: { load: { noBubble: !0 }, click: { setup: function (e) { var t = this || e; return we.test(t.type) && t.click && fe(t, "input") && He(t, "click", !0), !1 }, trigger: function (e) { var t = this || e; return we.test(t.type) && t.click && fe(t, "input") && He(t, "click"), !0 }, _default: function (e) { var t = e.target; return we.test(t.type) && t.click && fe(t, "input") && _.get(t, "click") || fe(t, "a") } }, beforeunload: { postDispatch: function (e) { void 0 !== e.result && e.originalEvent && (e.originalEvent.returnValue = e.result) } } } }, ce.removeEvent = function (e, t, n) { e.removeEventListener && e.removeEventListener(t, n) }, ce.Event = function (e, t) { if (!(this instanceof ce.Event)) return new ce.Event(e, t); e && e.type ? (this.originalEvent = e, this.type = e.type, this.isDefaultPrevented = e.defaultPrevented || void 0 === e.defaultPrevented && !1 === e.returnValue ? Ne : qe, this.target = e.target && 3 === e.target.nodeType ? e.target.parentNode : e.target, this.currentTarget = e.currentTarget, this.relatedTarget = e.relatedTarget) : this.type = e, t && ce.extend(this, t), this.timeStamp = e && e.timeStamp || Date.now(), this[ce.expando] = !0 }, ce.Event.prototype = { constructor: ce.Event, isDefaultPrevented: qe, isPropagationStopped: qe, isImmediatePropagationStopped: qe, isSimulated: !1, preventDefault: function () { var e = this.originalEvent; this.isDefaultPrevented = Ne, e && !this.isSimulated && e.preventDefault() }, stopPropagation: function () { var e = this.originalEvent; this.isPropagationStopped = Ne, e && !this.isSimulated && e.stopPropagation() }, stopImmediatePropagation: function () { var e = this.originalEvent; this.isImmediatePropagationStopped = Ne, e && !this.isSimulated && e.stopImmediatePropagation(), this.stopPropagation() } }, ce.each({ altKey: !0, bubbles: !0, cancelable: !0, changedTouches: !0, ctrlKey: !0, detail: !0, eventPhase: !0, metaKey: !0, pageX: !0, pageY: !0, shiftKey: !0, view: !0, "char": !0, code: !0, charCode: !0, key: !0, keyCode: !0, button: !0, buttons: !0, clientX: !0, clientY: !0, offsetX: !0, offsetY: !0, pointerId: !0, pointerType: !0, screenX: !0, screenY: !0, targetTouches: !0, toElement: !0, touches: !0, which: !0 }, ce.event.addProp), ce.each({ focus: "focusin", blur: "focusout" }, function (r, i) { function o(e) { if (C.documentMode) { var t = _.get(this, "handle"), n = ce.event.fix(e); n.type = "focusin" === e.type ? "focus" : "blur", n.isSimulated = !0, t(e), n.target === n.currentTarget && t(n) } else ce.event.simulate(i, e.target, ce.event.fix(e)) } ce.event.special[r] = { setup: function () { var e; if (He(this, r, !0), !C.documentMode) return !1; (e = _.get(this, i)) || this.addEventListener(i, o), _.set(this, i, (e || 0) + 1) }, trigger: function () { return He(this, r), !0 }, teardown: function () { var e; if (!C.documentMode) return !1; (e = _.get(this, i) - 1) ? _.set(this, i, e) : (this.removeEventListener(i, o), _.remove(this, i)) }, _default: function (e) { return _.get(e.target, r) }, delegateType: i }, ce.event.special[i] = { setup: function () { var e = this.ownerDocument || this.document || this, t = C.documentMode ? this : e, n = _.get(t, i); n || (C.documentMode ? this.addEventListener(i, o) : e.addEventListener(r, o, !0)), _.set(t, i, (n || 0) + 1) }, teardown: function () { var e = this.ownerDocument || this.document || this, t = C.documentMode ? this : e, n = _.get(t, i) - 1; n ? _.set(t, i, n) : (C.documentMode ? this.removeEventListener(i, o) : e.removeEventListener(r, o, !0), _.remove(t, i)) } } }), ce.each({ mouseenter: "mouseover", mouseleave: "mouseout", pointerenter: "pointerover", pointerleave: "pointerout" }, function (e, i) { ce.event.special[e] = { delegateType: i, bindType: i, handle: function (e) { var t, n = e.relatedTarget, r = e.handleObj; return n && (n === this || ce.contains(this, n)) || (e.type = r.origType, t = r.handler.apply(this, arguments), e.type = i), t } } }), ce.fn.extend({ on: function (e, t, n, r) { return Le(this, e, t, n, r) }, one: function (e, t, n, r) { return Le(this, e, t, n, r, 1) }, off: function (e, t, n) { var r, i; if (e && e.preventDefault && e.handleObj) return r = e.handleObj, ce(e.delegateTarget).off(r.namespace ? r.origType + "." + r.namespace : r.origType, r.selector, r.handler), this; if ("object" == typeof e) { for (i in e) this.off(i, t, e[i]); return this } return !1 !== t && "function" != typeof t || (n = t, t = void 0), !1 === n && (n = qe), this.each(function () { ce.event.remove(this, e, n, t) }) } }); var Oe = /\s*$/g; function Me(e, t) { return fe(e, "table") && fe(11 !== t.nodeType ? t : t.firstChild, "tr") && ce(e).children("tbody")[0] || e } function Ie(e) { return e.type = (null !== e.getAttribute("type")) + "/" + e.type, e } function We(e) { return "true/" === (e.type || "").slice(0, 5) ? e.type = e.type.slice(5) : e.removeAttribute("type"), e } function Fe(e, t) { var n, r, i, o, a, s; if (1 === t.nodeType) { if (_.hasData(e) && (s = _.get(e).events)) for (i in _.remove(t, "handle events"), s) for (n = 0, r = s[i].length; n < r; n++)ce.event.add(t, i, s[i][n]); X.hasData(e) && (o = X.access(e), a = ce.extend({}, o), X.set(t, a)) } } function $e(n, r, i, o) { r = g(r); var e, t, a, s, u, l, c = 0, f = n.length, p = f - 1, d = r[0], h = v(d); if (h || 1 < f && "string" == typeof d && !le.checkClone && Pe.test(d)) return n.each(function (e) { var t = n.eq(e); h && (r[0] = d.call(this, e, t.html())), $e(t, r, i, o) }); if (f && (t = (e = Ae(r, n[0].ownerDocument, !1, n, o)).firstChild, 1 === e.childNodes.length && (e = t), t || o)) { for (s = (a = ce.map(Se(e, "script"), Ie)).length; c < f; c++)u = e, c !== p && (u = ce.clone(u, !0, !0), s && ce.merge(a, Se(u, "script"))), i.call(n[c], u, c); if (s) for (l = a[a.length - 1].ownerDocument, ce.map(a, We), c = 0; c < s; c++)u = a[c], Ce.test(u.type || "") && !_.access(u, "globalEval") && ce.contains(l, u) && (u.src && "module" !== (u.type || "").toLowerCase() ? ce._evalUrl && !u.noModule && ce._evalUrl(u.src, { nonce: u.nonce || u.getAttribute("nonce") }, l) : m(u.textContent.replace(Re, ""), u, l)) } return n } function Be(e, t, n) { for (var r, i = t ? ce.filter(t, e) : e, o = 0; null != (r = i[o]); o++)n || 1 !== r.nodeType || ce.cleanData(Se(r)), r.parentNode && (n && K(r) && Ee(Se(r, "script")), r.parentNode.removeChild(r)); return e } ce.extend({ htmlPrefilter: function (e) { return e }, clone: function (e, t, n) { var r, i, o, a, s, u, l, c = e.cloneNode(!0), f = K(e); if (!(le.noCloneChecked || 1 !== e.nodeType && 11 !== e.nodeType || ce.isXMLDoc(e))) for (a = Se(c), r = 0, i = (o = Se(e)).length; r < i; r++)s = o[r], u = a[r], void 0, "input" === (l = u.nodeName.toLowerCase()) && we.test(s.type) ? u.checked = s.checked : "input" !== l && "textarea" !== l || (u.defaultValue = s.defaultValue); if (t) if (n) for (o = o || Se(e), a = a || Se(c), r = 0, i = o.length; r < i; r++)Fe(o[r], a[r]); else Fe(e, c); return 0 < (a = Se(c, "script")).length && Ee(a, !f && Se(e, "script")), c }, cleanData: function (e) { for (var t, n, r, i = ce.event.special, o = 0; void 0 !== (n = e[o]); o++)if ($(n)) { if (t = n[_.expando]) { if (t.events) for (r in t.events) i[r] ? ce.event.remove(n, r) : ce.removeEvent(n, r, t.handle); n[_.expando] = void 0 } n[X.expando] && (n[X.expando] = void 0) } } }), ce.fn.extend({ detach: function (e) { return Be(this, e, !0) }, remove: function (e) { return Be(this, e) }, text: function (e) { return R(this, function (e) { return void 0 === e ? ce.text(this) : this.empty().each(function () { 1 !== this.nodeType && 11 !== this.nodeType && 9 !== this.nodeType || (this.textContent = e) }) }, null, e, arguments.length) }, append: function () { return $e(this, arguments, function (e) { 1 !== this.nodeType && 11 !== this.nodeType && 9 !== this.nodeType || Me(this, e).appendChild(e) }) }, prepend: function () { return $e(this, arguments, function (e) { if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) { var t = Me(this, e); t.insertBefore(e, t.firstChild) } }) }, before: function () { return $e(this, arguments, function (e) { this.parentNode && this.parentNode.insertBefore(e, this) }) }, after: function () { return $e(this, arguments, function (e) { this.parentNode && this.parentNode.insertBefore(e, this.nextSibling) }) }, empty: function () { for (var e, t = 0; null != (e = this[t]); t++)1 === e.nodeType && (ce.cleanData(Se(e, !1)), e.textContent = ""); return this }, clone: function (e, t) { return e = null != e && e, t = null == t ? e : t, this.map(function () { return ce.clone(this, e, t) }) }, html: function (e) { return R(this, function (e) { var t = this[0] || {}, n = 0, r = this.length; if (void 0 === e && 1 === t.nodeType) return t.innerHTML; if ("string" == typeof e && !Oe.test(e) && !ke[(Te.exec(e) || ["", ""])[1].toLowerCase()]) { e = ce.htmlPrefilter(e); try { for (; n < r; n++)1 === (t = this[n] || {}).nodeType && (ce.cleanData(Se(t, !1)), t.innerHTML = e); t = 0 } catch (e) { } } t && this.empty().append(e) }, null, e, arguments.length) }, replaceWith: function () { var n = []; return $e(this, arguments, function (e) { var t = this.parentNode; ce.inArray(this, n) < 0 && (ce.cleanData(Se(this)), t && t.replaceChild(e, this)) }, n) } }), ce.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function (e, a) { ce.fn[e] = function (e) { for (var t, n = [], r = ce(e), i = r.length - 1, o = 0; o <= i; o++)t = o === i ? this : this.clone(!0), ce(r[o])[a](t), s.apply(n, t.get()); return this.pushStack(n) } }); var _e = new RegExp("^(" + G + ")(?!px)[a-z%]+$", "i"), Xe = /^--/, Ue = function (e) { var t = e.ownerDocument.defaultView; return t && t.opener || (t = ie), t.getComputedStyle(e) }, ze = function (e, t, n) { var r, i, o = {}; for (i in t) o[i] = e.style[i], e.style[i] = t[i]; for (i in r = n.call(e), t) e.style[i] = o[i]; return r }, Ve = new RegExp(Q.join("|"), "i"); function Ge(e, t, n) { var r, i, o, a, s = Xe.test(t), u = e.style; return (n = n || Ue(e)) && (a = n.getPropertyValue(t) || n[t], s && a && (a = a.replace(ve, "$1") || void 0), "" !== a || K(e) || (a = ce.style(e, t)), !le.pixelBoxStyles() && _e.test(a) && Ve.test(t) && (r = u.width, i = u.minWidth, o = u.maxWidth, u.minWidth = u.maxWidth = u.width = a, a = n.width, u.width = r, u.minWidth = i, u.maxWidth = o)), void 0 !== a ? a + "" : a } function Ye(e, t) { return { get: function () { if (!e()) return (this.get = t).apply(this, arguments); delete this.get } } } !function () { function e() { if (l) { u.style.cssText = "position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0", l.style.cssText = "position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%", J.appendChild(u).appendChild(l); var e = ie.getComputedStyle(l); n = "1%" !== e.top, s = 12 === t(e.marginLeft), l.style.right = "60%", o = 36 === t(e.right), r = 36 === t(e.width), l.style.position = "absolute", i = 12 === t(l.offsetWidth / 3), J.removeChild(u), l = null } } function t(e) { return Math.round(parseFloat(e)) } var n, r, i, o, a, s, u = C.createElement("div"), l = C.createElement("div"); l.style && (l.style.backgroundClip = "content-box", l.cloneNode(!0).style.backgroundClip = "", le.clearCloneStyle = "content-box" === l.style.backgroundClip, ce.extend(le, { boxSizingReliable: function () { return e(), r }, pixelBoxStyles: function () { return e(), o }, pixelPosition: function () { return e(), n }, reliableMarginLeft: function () { return e(), s }, scrollboxSize: function () { return e(), i }, reliableTrDimensions: function () { var e, t, n, r; return null == a && (e = C.createElement("table"), t = C.createElement("tr"), n = C.createElement("div"), e.style.cssText = "position:absolute;left:-11111px;border-collapse:separate", t.style.cssText = "border:1px solid", t.style.height = "1px", n.style.height = "9px", n.style.display = "block", J.appendChild(e).appendChild(t).appendChild(n), r = ie.getComputedStyle(t), a = parseInt(r.height, 10) + parseInt(r.borderTopWidth, 10) + parseInt(r.borderBottomWidth, 10) === t.offsetHeight, J.removeChild(e)), a } })) }(); var Qe = ["Webkit", "Moz", "ms"], Je = C.createElement("div").style, Ke = {}; function Ze(e) { var t = ce.cssProps[e] || Ke[e]; return t || (e in Je ? e : Ke[e] = function (e) { var t = e[0].toUpperCase() + e.slice(1), n = Qe.length; while (n--) if ((e = Qe[n] + t) in Je) return e }(e) || e) } var et = /^(none|table(?!-c[ea]).+)/, tt = { position: "absolute", visibility: "hidden", display: "block" }, nt = { letterSpacing: "0", fontWeight: "400" }; function rt(e, t, n) { var r = Y.exec(t); return r ? Math.max(0, r[2] - (n || 0)) + (r[3] || "px") : t } function it(e, t, n, r, i, o) { var a = "width" === t ? 1 : 0, s = 0, u = 0, l = 0; if (n === (r ? "border" : "content")) return 0; for (; a < 4; a += 2)"margin" === n && (l += ce.css(e, n + Q[a], !0, i)), r ? ("content" === n && (u -= ce.css(e, "padding" + Q[a], !0, i)), "margin" !== n && (u -= ce.css(e, "border" + Q[a] + "Width", !0, i))) : (u += ce.css(e, "padding" + Q[a], !0, i), "padding" !== n ? u += ce.css(e, "border" + Q[a] + "Width", !0, i) : s += ce.css(e, "border" + Q[a] + "Width", !0, i)); return !r && 0 <= o && (u += Math.max(0, Math.ceil(e["offset" + t[0].toUpperCase() + t.slice(1)] - o - u - s - .5)) || 0), u + l } function ot(e, t, n) { var r = Ue(e), i = (!le.boxSizingReliable() || n) && "border-box" === ce.css(e, "boxSizing", !1, r), o = i, a = Ge(e, t, r), s = "offset" + t[0].toUpperCase() + t.slice(1); if (_e.test(a)) { if (!n) return a; a = "auto" } return (!le.boxSizingReliable() && i || !le.reliableTrDimensions() && fe(e, "tr") || "auto" === a || !parseFloat(a) && "inline" === ce.css(e, "display", !1, r)) && e.getClientRects().length && (i = "border-box" === ce.css(e, "boxSizing", !1, r), (o = s in e) && (a = e[s])), (a = parseFloat(a) || 0) + it(e, t, n || (i ? "border" : "content"), o, r, a) + "px" } function at(e, t, n, r, i) { return new at.prototype.init(e, t, n, r, i) } ce.extend({ cssHooks: { opacity: { get: function (e, t) { if (t) { var n = Ge(e, "opacity"); return "" === n ? "1" : n } } } }, cssNumber: { animationIterationCount: !0, aspectRatio: !0, borderImageSlice: !0, columnCount: !0, flexGrow: !0, flexShrink: !0, fontWeight: !0, gridArea: !0, gridColumn: !0, gridColumnEnd: !0, gridColumnStart: !0, gridRow: !0, gridRowEnd: !0, gridRowStart: !0, lineHeight: !0, opacity: !0, order: !0, orphans: !0, scale: !0, widows: !0, zIndex: !0, zoom: !0, fillOpacity: !0, floodOpacity: !0, stopOpacity: !0, strokeMiterlimit: !0, strokeOpacity: !0 }, cssProps: {}, style: function (e, t, n, r) { if (e && 3 !== e.nodeType && 8 !== e.nodeType && e.style) { var i, o, a, s = F(t), u = Xe.test(t), l = e.style; if (u || (t = Ze(s)), a = ce.cssHooks[t] || ce.cssHooks[s], void 0 === n) return a && "get" in a && void 0 !== (i = a.get(e, !1, r)) ? i : l[t]; "string" === (o = typeof n) && (i = Y.exec(n)) && i[1] && (n = te(e, t, i), o = "number"), null != n && n == n && ("number" !== o || u || (n += i && i[3] || (ce.cssNumber[s] ? "" : "px")), le.clearCloneStyle || "" !== n || 0 !== t.indexOf("background") || (l[t] = "inherit"), a && "set" in a && void 0 === (n = a.set(e, n, r)) || (u ? l.setProperty(t, n) : l[t] = n)) } }, css: function (e, t, n, r) { var i, o, a, s = F(t); return Xe.test(t) || (t = Ze(s)), (a = ce.cssHooks[t] || ce.cssHooks[s]) && "get" in a && (i = a.get(e, !0, n)), void 0 === i && (i = Ge(e, t, r)), "normal" === i && t in nt && (i = nt[t]), "" === n || n ? (o = parseFloat(i), !0 === n || isFinite(o) ? o || 0 : i) : i } }), ce.each(["height", "width"], function (e, u) { ce.cssHooks[u] = { get: function (e, t, n) { if (t) return !et.test(ce.css(e, "display")) || e.getClientRects().length && e.getBoundingClientRect().width ? ot(e, u, n) : ze(e, tt, function () { return ot(e, u, n) }) }, set: function (e, t, n) { var r, i = Ue(e), o = !le.scrollboxSize() && "absolute" === i.position, a = (o || n) && "border-box" === ce.css(e, "boxSizing", !1, i), s = n ? it(e, u, n, a, i) : 0; return a && o && (s -= Math.ceil(e["offset" + u[0].toUpperCase() + u.slice(1)] - parseFloat(i[u]) - it(e, u, "border", !1, i) - .5)), s && (r = Y.exec(t)) && "px" !== (r[3] || "px") && (e.style[u] = t, t = ce.css(e, u)), rt(0, t, s) } } }), ce.cssHooks.marginLeft = Ye(le.reliableMarginLeft, function (e, t) { if (t) return (parseFloat(Ge(e, "marginLeft")) || e.getBoundingClientRect().left - ze(e, { marginLeft: 0 }, function () { return e.getBoundingClientRect().left })) + "px" }), ce.each({ margin: "", padding: "", border: "Width" }, function (i, o) { ce.cssHooks[i + o] = { expand: function (e) { for (var t = 0, n = {}, r = "string" == typeof e ? e.split(" ") : [e]; t < 4; t++)n[i + Q[t] + o] = r[t] || r[t - 2] || r[0]; return n } }, "margin" !== i && (ce.cssHooks[i + o].set = rt) }), ce.fn.extend({ css: function (e, t) { return R(this, function (e, t, n) { var r, i, o = {}, a = 0; if (Array.isArray(t)) { for (r = Ue(e), i = t.length; a < i; a++)o[t[a]] = ce.css(e, t[a], !1, r); return o } return void 0 !== n ? ce.style(e, t, n) : ce.css(e, t) }, e, t, 1 < arguments.length) } }), ((ce.Tween = at).prototype = { constructor: at, init: function (e, t, n, r, i, o) { this.elem = e, this.prop = n, this.easing = i || ce.easing._default, this.options = t, this.start = this.now = this.cur(), this.end = r, this.unit = o || (ce.cssNumber[n] ? "" : "px") }, cur: function () { var e = at.propHooks[this.prop]; return e && e.get ? e.get(this) : at.propHooks._default.get(this) }, run: function (e) { var t, n = at.propHooks[this.prop]; return this.options.duration ? this.pos = t = ce.easing[this.easing](e, this.options.duration * e, 0, 1, this.options.duration) : this.pos = t = e, this.now = (this.end - this.start) * t + this.start, this.options.step && this.options.step.call(this.elem, this.now, this), n && n.set ? n.set(this) : at.propHooks._default.set(this), this } }).init.prototype = at.prototype, (at.propHooks = { _default: { get: function (e) { var t; return 1 !== e.elem.nodeType || null != e.elem[e.prop] && null == e.elem.style[e.prop] ? e.elem[e.prop] : (t = ce.css(e.elem, e.prop, "")) && "auto" !== t ? t : 0 }, set: function (e) { ce.fx.step[e.prop] ? ce.fx.step[e.prop](e) : 1 !== e.elem.nodeType || !ce.cssHooks[e.prop] && null == e.elem.style[Ze(e.prop)] ? e.elem[e.prop] = e.now : ce.style(e.elem, e.prop, e.now + e.unit) } } }).scrollTop = at.propHooks.scrollLeft = { set: function (e) { e.elem.nodeType && e.elem.parentNode && (e.elem[e.prop] = e.now) } }, ce.easing = { linear: function (e) { return e }, swing: function (e) { return .5 - Math.cos(e * Math.PI) / 2 }, _default: "swing" }, ce.fx = at.prototype.init, ce.fx.step = {}; var st, ut, lt, ct, ft = /^(?:toggle|show|hide)$/, pt = /queueHooks$/; function dt() { ut && (!1 === C.hidden && ie.requestAnimationFrame ? ie.requestAnimationFrame(dt) : ie.setTimeout(dt, ce.fx.interval), ce.fx.tick()) } function ht() { return ie.setTimeout(function () { st = void 0 }), st = Date.now() } function gt(e, t) { var n, r = 0, i = { height: e }; for (t = t ? 1 : 0; r < 4; r += 2 - t)i["margin" + (n = Q[r])] = i["padding" + n] = e; return t && (i.opacity = i.width = e), i } function vt(e, t, n) { for (var r, i = (yt.tweeners[t] || []).concat(yt.tweeners["*"]), o = 0, a = i.length; o < a; o++)if (r = i[o].call(n, t, e)) return r } function yt(o, e, t) { var n, a, r = 0, i = yt.prefilters.length, s = ce.Deferred().always(function () { delete u.elem }), u = function () { if (a) return !1; for (var e = st || ht(), t = Math.max(0, l.startTime + l.duration - e), n = 1 - (t / l.duration || 0), r = 0, i = l.tweens.length; r < i; r++)l.tweens[r].run(n); return s.notifyWith(o, [l, n, t]), n < 1 && i ? t : (i || s.notifyWith(o, [l, 1, 0]), s.resolveWith(o, [l]), !1) }, l = s.promise({ elem: o, props: ce.extend({}, e), opts: ce.extend(!0, { specialEasing: {}, easing: ce.easing._default }, t), originalProperties: e, originalOptions: t, startTime: st || ht(), duration: t.duration, tweens: [], createTween: function (e, t) { var n = ce.Tween(o, l.opts, e, t, l.opts.specialEasing[e] || l.opts.easing); return l.tweens.push(n), n }, stop: function (e) { var t = 0, n = e ? l.tweens.length : 0; if (a) return this; for (a = !0; t < n; t++)l.tweens[t].run(1); return e ? (s.notifyWith(o, [l, 1, 0]), s.resolveWith(o, [l, e])) : s.rejectWith(o, [l, e]), this } }), c = l.props; for (!function (e, t) { var n, r, i, o, a; for (n in e) if (i = t[r = F(n)], o = e[n], Array.isArray(o) && (i = o[1], o = e[n] = o[0]), n !== r && (e[r] = o, delete e[n]), (a = ce.cssHooks[r]) && "expand" in a) for (n in o = a.expand(o), delete e[r], o) n in e || (e[n] = o[n], t[n] = i); else t[r] = i }(c, l.opts.specialEasing); r < i; r++)if (n = yt.prefilters[r].call(l, o, c, l.opts)) return v(n.stop) && (ce._queueHooks(l.elem, l.opts.queue).stop = n.stop.bind(n)), n; return ce.map(c, vt, l), v(l.opts.start) && l.opts.start.call(o, l), l.progress(l.opts.progress).done(l.opts.done, l.opts.complete).fail(l.opts.fail).always(l.opts.always), ce.fx.timer(ce.extend(u, { elem: o, anim: l, queue: l.opts.queue })), l } ce.Animation = ce.extend(yt, { tweeners: { "*": [function (e, t) { var n = this.createTween(e, t); return te(n.elem, e, Y.exec(t), n), n }] }, tweener: function (e, t) { v(e) ? (t = e, e = ["*"]) : e = e.match(D); for (var n, r = 0, i = e.length; r < i; r++)n = e[r], yt.tweeners[n] = yt.tweeners[n] || [], yt.tweeners[n].unshift(t) }, prefilters: [function (e, t, n) { var r, i, o, a, s, u, l, c, f = "width" in t || "height" in t, p = this, d = {}, h = e.style, g = e.nodeType && ee(e), v = _.get(e, "fxshow"); for (r in n.queue || (null == (a = ce._queueHooks(e, "fx")).unqueued && (a.unqueued = 0, s = a.empty.fire, a.empty.fire = function () { a.unqueued || s() }), a.unqueued++, p.always(function () { p.always(function () { a.unqueued--, ce.queue(e, "fx").length || a.empty.fire() }) })), t) if (i = t[r], ft.test(i)) { if (delete t[r], o = o || "toggle" === i, i === (g ? "hide" : "show")) { if ("show" !== i || !v || void 0 === v[r]) continue; g = !0 } d[r] = v && v[r] || ce.style(e, r) } if ((u = !ce.isEmptyObject(t)) || !ce.isEmptyObject(d)) for (r in f && 1 === e.nodeType && (n.overflow = [h.overflow, h.overflowX, h.overflowY], null == (l = v && v.display) && (l = _.get(e, "display")), "none" === (c = ce.css(e, "display")) && (l ? c = l : (re([e], !0), l = e.style.display || l, c = ce.css(e, "display"), re([e]))), ("inline" === c || "inline-block" === c && null != l) && "none" === ce.css(e, "float") && (u || (p.done(function () { h.display = l }), null == l && (c = h.display, l = "none" === c ? "" : c)), h.display = "inline-block")), n.overflow && (h.overflow = "hidden", p.always(function () { h.overflow = n.overflow[0], h.overflowX = n.overflow[1], h.overflowY = n.overflow[2] })), u = !1, d) u || (v ? "hidden" in v && (g = v.hidden) : v = _.access(e, "fxshow", { display: l }), o && (v.hidden = !g), g && re([e], !0), p.done(function () { for (r in g || re([e]), _.remove(e, "fxshow"), d) ce.style(e, r, d[r]) })), u = vt(g ? v[r] : 0, r, p), r in v || (v[r] = u.start, g && (u.end = u.start, u.start = 0)) }], prefilter: function (e, t) { t ? yt.prefilters.unshift(e) : yt.prefilters.push(e) } }), ce.speed = function (e, t, n) { var r = e && "object" == typeof e ? ce.extend({}, e) : { complete: n || !n && t || v(e) && e, duration: e, easing: n && t || t && !v(t) && t }; return ce.fx.off ? r.duration = 0 : "number" != typeof r.duration && (r.duration in ce.fx.speeds ? r.duration = ce.fx.speeds[r.duration] : r.duration = ce.fx.speeds._default), null != r.queue && !0 !== r.queue || (r.queue = "fx"), r.old = r.complete, r.complete = function () { v(r.old) && r.old.call(this), r.queue && ce.dequeue(this, r.queue) }, r }, ce.fn.extend({ fadeTo: function (e, t, n, r) { return this.filter(ee).css("opacity", 0).show().end().animate({ opacity: t }, e, n, r) }, animate: function (t, e, n, r) { var i = ce.isEmptyObject(t), o = ce.speed(e, n, r), a = function () { var e = yt(this, ce.extend({}, t), o); (i || _.get(this, "finish")) && e.stop(!0) }; return a.finish = a, i || !1 === o.queue ? this.each(a) : this.queue(o.queue, a) }, stop: function (i, e, o) { var a = function (e) { var t = e.stop; delete e.stop, t(o) }; return "string" != typeof i && (o = e, e = i, i = void 0), e && this.queue(i || "fx", []), this.each(function () { var e = !0, t = null != i && i + "queueHooks", n = ce.timers, r = _.get(this); if (t) r[t] && r[t].stop && a(r[t]); else for (t in r) r[t] && r[t].stop && pt.test(t) && a(r[t]); for (t = n.length; t--;)n[t].elem !== this || null != i && n[t].queue !== i || (n[t].anim.stop(o), e = !1, n.splice(t, 1)); !e && o || ce.dequeue(this, i) }) }, finish: function (a) { return !1 !== a && (a = a || "fx"), this.each(function () { var e, t = _.get(this), n = t[a + "queue"], r = t[a + "queueHooks"], i = ce.timers, o = n ? n.length : 0; for (t.finish = !0, ce.queue(this, a, []), r && r.stop && r.stop.call(this, !0), e = i.length; e--;)i[e].elem === this && i[e].queue === a && (i[e].anim.stop(!0), i.splice(e, 1)); for (e = 0; e < o; e++)n[e] && n[e].finish && n[e].finish.call(this); delete t.finish }) } }), ce.each(["toggle", "show", "hide"], function (e, r) { var i = ce.fn[r]; ce.fn[r] = function (e, t, n) { return null == e || "boolean" == typeof e ? i.apply(this, arguments) : this.animate(gt(r, !0), e, t, n) } }), ce.each({ slideDown: gt("show"), slideUp: gt("hide"), slideToggle: gt("toggle"), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } }, function (e, r) { ce.fn[e] = function (e, t, n) { return this.animate(r, e, t, n) } }), ce.timers = [], ce.fx.tick = function () { var e, t = 0, n = ce.timers; for (st = Date.now(); t < n.length; t++)(e = n[t])() || n[t] !== e || n.splice(t--, 1); n.length || ce.fx.stop(), st = void 0 }, ce.fx.timer = function (e) { ce.timers.push(e), ce.fx.start() }, ce.fx.interval = 13, ce.fx.start = function () { ut || (ut = !0, dt()) }, ce.fx.stop = function () { ut = null }, ce.fx.speeds = { slow: 600, fast: 200, _default: 400 }, ce.fn.delay = function (r, e) { return r = ce.fx && ce.fx.speeds[r] || r, e = e || "fx", this.queue(e, function (e, t) { var n = ie.setTimeout(e, r); t.stop = function () { ie.clearTimeout(n) } }) }, lt = C.createElement("input"), ct = C.createElement("select").appendChild(C.createElement("option")), lt.type = "checkbox", le.checkOn = "" !== lt.value, le.optSelected = ct.selected, (lt = C.createElement("input")).value = "t", lt.type = "radio", le.radioValue = "t" === lt.value; var mt, xt = ce.expr.attrHandle; ce.fn.extend({ attr: function (e, t) { return R(this, ce.attr, e, t, 1 < arguments.length) }, removeAttr: function (e) { return this.each(function () { ce.removeAttr(this, e) }) } }), ce.extend({ attr: function (e, t, n) { var r, i, o = e.nodeType; if (3 !== o && 8 !== o && 2 !== o) return "undefined" == typeof e.getAttribute ? ce.prop(e, t, n) : (1 === o && ce.isXMLDoc(e) || (i = ce.attrHooks[t.toLowerCase()] || (ce.expr.match.bool.test(t) ? mt : void 0)), void 0 !== n ? null === n ? void ce.removeAttr(e, t) : i && "set" in i && void 0 !== (r = i.set(e, n, t)) ? r : (e.setAttribute(t, n + ""), n) : i && "get" in i && null !== (r = i.get(e, t)) ? r : null == (r = ce.find.attr(e, t)) ? void 0 : r) }, attrHooks: { type: { set: function (e, t) { if (!le.radioValue && "radio" === t && fe(e, "input")) { var n = e.value; return e.setAttribute("type", t), n && (e.value = n), t } } } }, removeAttr: function (e, t) { var n, r = 0, i = t && t.match(D); if (i && 1 === e.nodeType) while (n = i[r++]) e.removeAttribute(n) } }), mt = { set: function (e, t, n) { return !1 === t ? ce.removeAttr(e, n) : e.setAttribute(n, n), n } }, ce.each(ce.expr.match.bool.source.match(/\w+/g), function (e, t) { var a = xt[t] || ce.find.attr; xt[t] = function (e, t, n) { var r, i, o = t.toLowerCase(); return n || (i = xt[o], xt[o] = r, r = null != a(e, t, n) ? o : null, xt[o] = i), r } }); var bt = /^(?:input|select|textarea|button)$/i, wt = /^(?:a|area)$/i; function Tt(e) { return (e.match(D) || []).join(" ") } function Ct(e) { return e.getAttribute && e.getAttribute("class") || "" } function kt(e) { return Array.isArray(e) ? e : "string" == typeof e && e.match(D) || [] } ce.fn.extend({ prop: function (e, t) { return R(this, ce.prop, e, t, 1 < arguments.length) }, removeProp: function (e) { return this.each(function () { delete this[ce.propFix[e] || e] }) } }), ce.extend({ prop: function (e, t, n) { var r, i, o = e.nodeType; if (3 !== o && 8 !== o && 2 !== o) return 1 === o && ce.isXMLDoc(e) || (t = ce.propFix[t] || t, i = ce.propHooks[t]), void 0 !== n ? i && "set" in i && void 0 !== (r = i.set(e, n, t)) ? r : e[t] = n : i && "get" in i && null !== (r = i.get(e, t)) ? r : e[t] }, propHooks: { tabIndex: { get: function (e) { var t = ce.find.attr(e, "tabindex"); return t ? parseInt(t, 10) : bt.test(e.nodeName) || wt.test(e.nodeName) && e.href ? 0 : -1 } } }, propFix: { "for": "htmlFor", "class": "className" } }), le.optSelected || (ce.propHooks.selected = { get: function (e) { var t = e.parentNode; return t && t.parentNode && t.parentNode.selectedIndex, null }, set: function (e) { var t = e.parentNode; t && (t.selectedIndex, t.parentNode && t.parentNode.selectedIndex) } }), ce.each(["tabIndex", "readOnly", "maxLength", "cellSpacing", "cellPadding", "rowSpan", "colSpan", "useMap", "frameBorder", "contentEditable"], function () { ce.propFix[this.toLowerCase()] = this }), ce.fn.extend({ addClass: function (t) { var e, n, r, i, o, a; return v(t) ? this.each(function (e) { ce(this).addClass(t.call(this, e, Ct(this))) }) : (e = kt(t)).length ? this.each(function () { if (r = Ct(this), n = 1 === this.nodeType && " " + Tt(r) + " ") { for (o = 0; o < e.length; o++)i = e[o], n.indexOf(" " + i + " ") < 0 && (n += i + " "); a = Tt(n), r !== a && this.setAttribute("class", a) } }) : this }, removeClass: function (t) { var e, n, r, i, o, a; return v(t) ? this.each(function (e) { ce(this).removeClass(t.call(this, e, Ct(this))) }) : arguments.length ? (e = kt(t)).length ? this.each(function () { if (r = Ct(this), n = 1 === this.nodeType && " " + Tt(r) + " ") { for (o = 0; o < e.length; o++) { i = e[o]; while (-1 < n.indexOf(" " + i + " ")) n = n.replace(" " + i + " ", " ") } a = Tt(n), r !== a && this.setAttribute("class", a) } }) : this : this.attr("class", "") }, toggleClass: function (t, n) { var e, r, i, o, a = typeof t, s = "string" === a || Array.isArray(t); return v(t) ? this.each(function (e) { ce(this).toggleClass(t.call(this, e, Ct(this), n), n) }) : "boolean" == typeof n && s ? n ? this.addClass(t) : this.removeClass(t) : (e = kt(t), this.each(function () { if (s) for (o = ce(this), i = 0; i < e.length; i++)r = e[i], o.hasClass(r) ? o.removeClass(r) : o.addClass(r); else void 0 !== t && "boolean" !== a || ((r = Ct(this)) && _.set(this, "__className__", r), this.setAttribute && this.setAttribute("class", r || !1 === t ? "" : _.get(this, "__className__") || "")) })) }, hasClass: function (e) { var t, n, r = 0; t = " " + e + " "; while (n = this[r++]) if (1 === n.nodeType && -1 < (" " + Tt(Ct(n)) + " ").indexOf(t)) return !0; return !1 } }); var St = /\r/g; ce.fn.extend({ val: function (n) { var r, e, i, t = this[0]; return arguments.length ? (i = v(n), this.each(function (e) { var t; 1 === this.nodeType && (null == (t = i ? n.call(this, e, ce(this).val()) : n) ? t = "" : "number" == typeof t ? t += "" : Array.isArray(t) && (t = ce.map(t, function (e) { return null == e ? "" : e + "" })), (r = ce.valHooks[this.type] || ce.valHooks[this.nodeName.toLowerCase()]) && "set" in r && void 0 !== r.set(this, t, "value") || (this.value = t)) })) : t ? (r = ce.valHooks[t.type] || ce.valHooks[t.nodeName.toLowerCase()]) && "get" in r && void 0 !== (e = r.get(t, "value")) ? e : "string" == typeof (e = t.value) ? e.replace(St, "") : null == e ? "" : e : void 0 } }), ce.extend({ valHooks: { option: { get: function (e) { var t = ce.find.attr(e, "value"); return null != t ? t : Tt(ce.text(e)) } }, select: { get: function (e) { var t, n, r, i = e.options, o = e.selectedIndex, a = "select-one" === e.type, s = a ? null : [], u = a ? o + 1 : i.length; for (r = o < 0 ? u : a ? o : 0; r < u; r++)if (((n = i[r]).selected || r === o) && !n.disabled && (!n.parentNode.disabled || !fe(n.parentNode, "optgroup"))) { if (t = ce(n).val(), a) return t; s.push(t) } return s }, set: function (e, t) { var n, r, i = e.options, o = ce.makeArray(t), a = i.length; while (a--) ((r = i[a]).selected = -1 < ce.inArray(ce.valHooks.option.get(r), o)) && (n = !0); return n || (e.selectedIndex = -1), o } } } }), ce.each(["radio", "checkbox"], function () { ce.valHooks[this] = { set: function (e, t) { if (Array.isArray(t)) return e.checked = -1 < ce.inArray(ce(e).val(), t) } }, le.checkOn || (ce.valHooks[this].get = function (e) { return null === e.getAttribute("value") ? "on" : e.value }) }); var Et = ie.location, jt = { guid: Date.now() }, At = /\?/; ce.parseXML = function (e) { var t, n; if (!e || "string" != typeof e) return null; try { t = (new ie.DOMParser).parseFromString(e, "text/xml") } catch (e) { } return n = t && t.getElementsByTagName("parsererror")[0], t && !n || ce.error("Invalid XML: " + (n ? ce.map(n.childNodes, function (e) { return e.textContent }).join("\n") : e)), t }; var Dt = /^(?:focusinfocus|focusoutblur)$/, Nt = function (e) { e.stopPropagation() }; ce.extend(ce.event, { trigger: function (e, t, n, r) { var i, o, a, s, u, l, c, f, p = [n || C], d = ue.call(e, "type") ? e.type : e, h = ue.call(e, "namespace") ? e.namespace.split(".") : []; if (o = f = a = n = n || C, 3 !== n.nodeType && 8 !== n.nodeType && !Dt.test(d + ce.event.triggered) && (-1 < d.indexOf(".") && (d = (h = d.split(".")).shift(), h.sort()), u = d.indexOf(":") < 0 && "on" + d, (e = e[ce.expando] ? e : new ce.Event(d, "object" == typeof e && e)).isTrigger = r ? 2 : 3, e.namespace = h.join("."), e.rnamespace = e.namespace ? new RegExp("(^|\\.)" + h.join("\\.(?:.*\\.|)") + "(\\.|$)") : null, e.result = void 0, e.target || (e.target = n), t = null == t ? [e] : ce.makeArray(t, [e]), c = ce.event.special[d] || {}, r || !c.trigger || !1 !== c.trigger.apply(n, t))) { if (!r && !c.noBubble && !y(n)) { for (s = c.delegateType || d, Dt.test(s + d) || (o = o.parentNode); o; o = o.parentNode)p.push(o), a = o; a === (n.ownerDocument || C) && p.push(a.defaultView || a.parentWindow || ie) } i = 0; while ((o = p[i++]) && !e.isPropagationStopped()) f = o, e.type = 1 < i ? s : c.bindType || d, (l = (_.get(o, "events") || Object.create(null))[e.type] && _.get(o, "handle")) && l.apply(o, t), (l = u && o[u]) && l.apply && $(o) && (e.result = l.apply(o, t), !1 === e.result && e.preventDefault()); return e.type = d, r || e.isDefaultPrevented() || c._default && !1 !== c._default.apply(p.pop(), t) || !$(n) || u && v(n[d]) && !y(n) && ((a = n[u]) && (n[u] = null), ce.event.triggered = d, e.isPropagationStopped() && f.addEventListener(d, Nt), n[d](), e.isPropagationStopped() && f.removeEventListener(d, Nt), ce.event.triggered = void 0, a && (n[u] = a)), e.result } }, simulate: function (e, t, n) { var r = ce.extend(new ce.Event, n, { type: e, isSimulated: !0 }); ce.event.trigger(r, null, t) } }), ce.fn.extend({ trigger: function (e, t) { return this.each(function () { ce.event.trigger(e, t, this) }) }, triggerHandler: function (e, t) { var n = this[0]; if (n) return ce.event.trigger(e, t, n, !0) } }); var qt = /\[\]$/, Lt = /\r?\n/g, Ht = /^(?:submit|button|image|reset|file)$/i, Ot = /^(?:input|select|textarea|keygen)/i; function Pt(n, e, r, i) { var t; if (Array.isArray(e)) ce.each(e, function (e, t) { r || qt.test(n) ? i(n, t) : Pt(n + "[" + ("object" == typeof t && null != t ? e : "") + "]", t, r, i) }); else if (r || "object" !== x(e)) i(n, e); else for (t in e) Pt(n + "[" + t + "]", e[t], r, i) } ce.param = function (e, t) { var n, r = [], i = function (e, t) { var n = v(t) ? t() : t; r[r.length] = encodeURIComponent(e) + "=" + encodeURIComponent(null == n ? "" : n) }; if (null == e) return ""; if (Array.isArray(e) || e.jquery && !ce.isPlainObject(e)) ce.each(e, function () { i(this.name, this.value) }); else for (n in e) Pt(n, e[n], t, i); return r.join("&") }, ce.fn.extend({ serialize: function () { return ce.param(this.serializeArray()) }, serializeArray: function () { return this.map(function () { var e = ce.prop(this, "elements"); return e ? ce.makeArray(e) : this }).filter(function () { var e = this.type; return this.name && !ce(this).is(":disabled") && Ot.test(this.nodeName) && !Ht.test(e) && (this.checked || !we.test(e)) }).map(function (e, t) { var n = ce(this).val(); return null == n ? null : Array.isArray(n) ? ce.map(n, function (e) { return { name: t.name, value: e.replace(Lt, "\r\n") } }) : { name: t.name, value: n.replace(Lt, "\r\n") } }).get() } }); var Rt = /%20/g, Mt = /#.*$/, It = /([?&])_=[^&]*/, Wt = /^(.*?):[ \t]*([^\r\n]*)$/gm, Ft = /^(?:GET|HEAD)$/, $t = /^\/\//, Bt = {}, _t = {}, Xt = "*/".concat("*"), Ut = C.createElement("a"); function zt(o) { return function (e, t) { "string" != typeof e && (t = e, e = "*"); var n, r = 0, i = e.toLowerCase().match(D) || []; if (v(t)) while (n = i[r++]) "+" === n[0] ? (n = n.slice(1) || "*", (o[n] = o[n] || []).unshift(t)) : (o[n] = o[n] || []).push(t) } } function Vt(t, i, o, a) { var s = {}, u = t === _t; function l(e) { var r; return s[e] = !0, ce.each(t[e] || [], function (e, t) { var n = t(i, o, a); return "string" != typeof n || u || s[n] ? u ? !(r = n) : void 0 : (i.dataTypes.unshift(n), l(n), !1) }), r } return l(i.dataTypes[0]) || !s["*"] && l("*") } function Gt(e, t) { var n, r, i = ce.ajaxSettings.flatOptions || {}; for (n in t) void 0 !== t[n] && ((i[n] ? e : r || (r = {}))[n] = t[n]); return r && ce.extend(!0, e, r), e } Ut.href = Et.href, ce.extend({ active: 0, lastModified: {}, etag: {}, ajaxSettings: { url: Et.href, type: "GET", isLocal: /^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Et.protocol), global: !0, processData: !0, async: !0, contentType: "application/x-www-form-urlencoded; charset=UTF-8", accepts: { "*": Xt, text: "text/plain", html: "text/html", xml: "application/xml, text/xml", json: "application/json, text/javascript" }, contents: { xml: /\bxml\b/, html: /\bhtml/, json: /\bjson\b/ }, responseFields: { xml: "responseXML", text: "responseText", json: "responseJSON" }, converters: { "* text": String, "text html": !0, "text json": JSON.parse, "text xml": ce.parseXML }, flatOptions: { url: !0, context: !0 } }, ajaxSetup: function (e, t) { return t ? Gt(Gt(e, ce.ajaxSettings), t) : Gt(ce.ajaxSettings, e) }, ajaxPrefilter: zt(Bt), ajaxTransport: zt(_t), ajax: function (e, t) { "object" == typeof e && (t = e, e = void 0), t = t || {}; var c, f, p, n, d, r, h, g, i, o, v = ce.ajaxSetup({}, t), y = v.context || v, m = v.context && (y.nodeType || y.jquery) ? ce(y) : ce.event, x = ce.Deferred(), b = ce.Callbacks("once memory"), w = v.statusCode || {}, a = {}, s = {}, u = "canceled", T = { readyState: 0, getResponseHeader: function (e) { var t; if (h) { if (!n) { n = {}; while (t = Wt.exec(p)) n[t[1].toLowerCase() + " "] = (n[t[1].toLowerCase() + " "] || []).concat(t[2]) } t = n[e.toLowerCase() + " "] } return null == t ? null : t.join(", ") }, getAllResponseHeaders: function () { return h ? p : null }, setRequestHeader: function (e, t) { return null == h && (e = s[e.toLowerCase()] = s[e.toLowerCase()] || e, a[e] = t), this }, overrideMimeType: function (e) { return null == h && (v.mimeType = e), this }, statusCode: function (e) { var t; if (e) if (h) T.always(e[T.status]); else for (t in e) w[t] = [w[t], e[t]]; return this }, abort: function (e) { var t = e || u; return c && c.abort(t), l(0, t), this } }; if (x.promise(T), v.url = ((e || v.url || Et.href) + "").replace($t, Et.protocol + "//"), v.type = t.method || t.type || v.method || v.type, v.dataTypes = (v.dataType || "*").toLowerCase().match(D) || [""], null == v.crossDomain) { r = C.createElement("a"); try { r.href = v.url, r.href = r.href, v.crossDomain = Ut.protocol + "//" + Ut.host != r.protocol + "//" + r.host } catch (e) { v.crossDomain = !0 } } if (v.data && v.processData && "string" != typeof v.data && (v.data = ce.param(v.data, v.traditional)), Vt(Bt, v, t, T), h) return T; for (i in (g = ce.event && v.global) && 0 == ce.active++ && ce.event.trigger("ajaxStart"), v.type = v.type.toUpperCase(), v.hasContent = !Ft.test(v.type), f = v.url.replace(Mt, ""), v.hasContent ? v.data && v.processData && 0 === (v.contentType || "").indexOf("application/x-www-form-urlencoded") && (v.data = v.data.replace(Rt, "+")) : (o = v.url.slice(f.length), v.data && (v.processData || "string" == typeof v.data) && (f += (At.test(f) ? "&" : "?") + v.data, delete v.data), !1 === v.cache && (f = f.replace(It, "$1"), o = (At.test(f) ? "&" : "?") + "_=" + jt.guid++ + o), v.url = f + o), v.ifModified && (ce.lastModified[f] && T.setRequestHeader("If-Modified-Since", ce.lastModified[f]), ce.etag[f] && T.setRequestHeader("If-None-Match", ce.etag[f])), (v.data && v.hasContent && !1 !== v.contentType || t.contentType) && T.setRequestHeader("Content-Type", v.contentType), T.setRequestHeader("Accept", v.dataTypes[0] && v.accepts[v.dataTypes[0]] ? v.accepts[v.dataTypes[0]] + ("*" !== v.dataTypes[0] ? ", " + Xt + "; q=0.01" : "") : v.accepts["*"]), v.headers) T.setRequestHeader(i, v.headers[i]); if (v.beforeSend && (!1 === v.beforeSend.call(y, T, v) || h)) return T.abort(); if (u = "abort", b.add(v.complete), T.done(v.success), T.fail(v.error), c = Vt(_t, v, t, T)) { if (T.readyState = 1, g && m.trigger("ajaxSend", [T, v]), h) return T; v.async && 0 < v.timeout && (d = ie.setTimeout(function () { T.abort("timeout") }, v.timeout)); try { h = !1, c.send(a, l) } catch (e) { if (h) throw e; l(-1, e) } } else l(-1, "No Transport"); function l(e, t, n, r) { var i, o, a, s, u, l = t; h || (h = !0, d && ie.clearTimeout(d), c = void 0, p = r || "", T.readyState = 0 < e ? 4 : 0, i = 200 <= e && e < 300 || 304 === e, n && (s = function (e, t, n) { var r, i, o, a, s = e.contents, u = e.dataTypes; while ("*" === u[0]) u.shift(), void 0 === r && (r = e.mimeType || t.getResponseHeader("Content-Type")); if (r) for (i in s) if (s[i] && s[i].test(r)) { u.unshift(i); break } if (u[0] in n) o = u[0]; else { for (i in n) { if (!u[0] || e.converters[i + " " + u[0]]) { o = i; break } a || (a = i) } o = o || a } if (o) return o !== u[0] && u.unshift(o), n[o] }(v, T, n)), !i && -1 < ce.inArray("script", v.dataTypes) && ce.inArray("json", v.dataTypes) < 0 && (v.converters["text script"] = function () { }), s = function (e, t, n, r) { var i, o, a, s, u, l = {}, c = e.dataTypes.slice(); if (c[1]) for (a in e.converters) l[a.toLowerCase()] = e.converters[a]; o = c.shift(); while (o) if (e.responseFields[o] && (n[e.responseFields[o]] = t), !u && r && e.dataFilter && (t = e.dataFilter(t, e.dataType)), u = o, o = c.shift()) if ("*" === o) o = u; else if ("*" !== u && u !== o) { if (!(a = l[u + " " + o] || l["* " + o])) for (i in l) if ((s = i.split(" "))[1] === o && (a = l[u + " " + s[0]] || l["* " + s[0]])) { !0 === a ? a = l[i] : !0 !== l[i] && (o = s[0], c.unshift(s[1])); break } if (!0 !== a) if (a && e["throws"]) t = a(t); else try { t = a(t) } catch (e) { return { state: "parsererror", error: a ? e : "No conversion from " + u + " to " + o } } } return { state: "success", data: t } }(v, s, T, i), i ? (v.ifModified && ((u = T.getResponseHeader("Last-Modified")) && (ce.lastModified[f] = u), (u = T.getResponseHeader("etag")) && (ce.etag[f] = u)), 204 === e || "HEAD" === v.type ? l = "nocontent" : 304 === e ? l = "notmodified" : (l = s.state, o = s.data, i = !(a = s.error))) : (a = l, !e && l || (l = "error", e < 0 && (e = 0))), T.status = e, T.statusText = (t || l) + "", i ? x.resolveWith(y, [o, l, T]) : x.rejectWith(y, [T, l, a]), T.statusCode(w), w = void 0, g && m.trigger(i ? "ajaxSuccess" : "ajaxError", [T, v, i ? o : a]), b.fireWith(y, [T, l]), g && (m.trigger("ajaxComplete", [T, v]), --ce.active || ce.event.trigger("ajaxStop"))) } return T }, getJSON: function (e, t, n) { return ce.get(e, t, n, "json") }, getScript: function (e, t) { return ce.get(e, void 0, t, "script") } }), ce.each(["get", "post"], function (e, i) { ce[i] = function (e, t, n, r) { return v(t) && (r = r || n, n = t, t = void 0), ce.ajax(ce.extend({ url: e, type: i, dataType: r, data: t, success: n }, ce.isPlainObject(e) && e)) } }), ce.ajaxPrefilter(function (e) { var t; for (t in e.headers) "content-type" === t.toLowerCase() && (e.contentType = e.headers[t] || "") }), ce._evalUrl = function (e, t, n) { return ce.ajax({ url: e, type: "GET", dataType: "script", cache: !0, async: !1, global: !1, converters: { "text script": function () { } }, dataFilter: function (e) { ce.globalEval(e, t, n) } }) }, ce.fn.extend({ wrapAll: function (e) { var t; return this[0] && (v(e) && (e = e.call(this[0])), t = ce(e, this[0].ownerDocument).eq(0).clone(!0), this[0].parentNode && t.insertBefore(this[0]), t.map(function () { var e = this; while (e.firstElementChild) e = e.firstElementChild; return e }).append(this)), this }, wrapInner: function (n) { return v(n) ? this.each(function (e) { ce(this).wrapInner(n.call(this, e)) }) : this.each(function () { var e = ce(this), t = e.contents(); t.length ? t.wrapAll(n) : e.append(n) }) }, wrap: function (t) { var n = v(t); return this.each(function (e) { ce(this).wrapAll(n ? t.call(this, e) : t) }) }, unwrap: function (e) { return this.parent(e).not("body").each(function () { ce(this).replaceWith(this.childNodes) }), this } }), ce.expr.pseudos.hidden = function (e) { return !ce.expr.pseudos.visible(e) }, ce.expr.pseudos.visible = function (e) { return !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length) }, ce.ajaxSettings.xhr = function () { try { return new ie.XMLHttpRequest } catch (e) { } }; var Yt = { 0: 200, 1223: 204 }, Qt = ce.ajaxSettings.xhr(); le.cors = !!Qt && "withCredentials" in Qt, le.ajax = Qt = !!Qt, ce.ajaxTransport(function (i) { var o, a; if (le.cors || Qt && !i.crossDomain) return { send: function (e, t) { var n, r = i.xhr(); if (r.open(i.type, i.url, i.async, i.username, i.password), i.xhrFields) for (n in i.xhrFields) r[n] = i.xhrFields[n]; for (n in i.mimeType && r.overrideMimeType && r.overrideMimeType(i.mimeType), i.crossDomain || e["X-Requested-With"] || (e["X-Requested-With"] = "XMLHttpRequest"), e) r.setRequestHeader(n, e[n]); o = function (e) { return function () { o && (o = a = r.onload = r.onerror = r.onabort = r.ontimeout = r.onreadystatechange = null, "abort" === e ? r.abort() : "error" === e ? "number" != typeof r.status ? t(0, "error") : t(r.status, r.statusText) : t(Yt[r.status] || r.status, r.statusText, "text" !== (r.responseType || "text") || "string" != typeof r.responseText ? { binary: r.response } : { text: r.responseText }, r.getAllResponseHeaders())) } }, r.onload = o(), a = r.onerror = r.ontimeout = o("error"), void 0 !== r.onabort ? r.onabort = a : r.onreadystatechange = function () { 4 === r.readyState && ie.setTimeout(function () { o && a() }) }, o = o("abort"); try { r.send(i.hasContent && i.data || null) } catch (e) { if (o) throw e } }, abort: function () { o && o() } } }), ce.ajaxPrefilter(function (e) { e.crossDomain && (e.contents.script = !1) }), ce.ajaxSetup({ accepts: { script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" }, contents: { script: /\b(?:java|ecma)script\b/ }, converters: { "text script": function (e) { return ce.globalEval(e), e } } }), ce.ajaxPrefilter("script", function (e) { void 0 === e.cache && (e.cache = !1), e.crossDomain && (e.type = "GET") }), ce.ajaxTransport("script", function (n) { var r, i; if (n.crossDomain || n.scriptAttrs) return { send: function (e, t) { r = ce(" - + + {% endblock head %} @@ -59,6 +59,14 @@
Controls
onclick="set_dimensions()">Set Dimensions +
+
+ +
+ Set/Edit Background + +
@@ -168,7 +176,7 @@
Controls
- +
@@ -218,6 +226,30 @@
+ + {% endblock content %} \ No newline at end of file diff --git a/netbox_floorplan/templates/netbox_floorplan/floorplan_view.html b/netbox_floorplan/templates/netbox_floorplan/floorplan_view.html index 2e7819f..0af2c30 100644 --- a/netbox_floorplan/templates/netbox_floorplan/floorplan_view.html +++ b/netbox_floorplan/templates/netbox_floorplan/floorplan_view.html @@ -6,8 +6,9 @@ {% load django_htmx %} {% block head %} {{ block.super }} - - + + + {% endblock head %} @@ -75,7 +76,7 @@
- +

diff --git a/netbox_floorplan/templates/netbox_floorplan/floorplanimage.html b/netbox_floorplan/templates/netbox_floorplan/floorplanimage.html new file mode 100644 index 0000000..30bb9e1 --- /dev/null +++ b/netbox_floorplan/templates/netbox_floorplan/floorplanimage.html @@ -0,0 +1,49 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load humanize %} +{% load plugins %} + + +{% block content %} +
+
+
+
Asset
+ + + + + + + + + + {% if object.external_url %} + + + + + {% else %} + + + + + + + + + {% endif %} +
id{{ object.id }}
Name{{ object.name|placeholder }}
External URL{{ object.external_url }}
Filename{{ object.filename }}
Size{{ object.size|filesizeformat }}
+
+ {% include 'inc/panels/custom_fields.html' %} +
+ {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock content %} \ No newline at end of file diff --git a/netbox_floorplan/templates/netbox_floorplan/floorplanimage_edit.html b/netbox_floorplan/templates/netbox_floorplan/floorplanimage_edit.html new file mode 100644 index 0000000..3da4967 --- /dev/null +++ b/netbox_floorplan/templates/netbox_floorplan/floorplanimage_edit.html @@ -0,0 +1,45 @@ +{% extends 'generic/object_edit.html' %} +{% load static %} +{% load form_helpers %} +{% load helpers %} + +{% block form %} +
+ {% render_field form.name %} +
+
+
+ +
+
+
+
+ {% render_field form.file %} +
+
+ {% render_field form.external_url %} +
+
+ +
+
+
Comments
+
+ {% render_field form.comments %} +
+ + {% render_field form.tags %} +
+
+{% endblock %} diff --git a/netbox_floorplan/urls.py b/netbox_floorplan/urls.py index ae15ca6..6e48005 100644 --- a/netbox_floorplan/urls.py +++ b/netbox_floorplan/urls.py @@ -3,16 +3,29 @@ from netbox.views.generic import ObjectChangeLogView urlpatterns = ( - path('floorplans/', views.FloorplanListView.as_view(), name='floorplan_list'), - path('floorplans/racks/', views.FloorplanRackListView.as_view(), - name='floorplan_rack_list'), - path('floorplans/devices/', views.FloorplanDeviceListView.as_view(), - name='floorplan_device_list'), - path('floorplans/add/', views.FloorplanAddView.as_view(), name='floorplan_add'), - path('floorplans//edit/', - views.FloorplanMapEditView.as_view(), name='floorplan_edit'), - path('floorplans//delete/', - views.FloorplanDeleteView.as_view(), name='floorplan_delete'), - path('floorplans//changelog/', ObjectChangeLogView.as_view(), - name='floorplan_changelog', kwargs={'model': models.Floorplan}) + path('floorplans/', views.FloorplanListView.as_view(), + name='floorplan_list'), + path('floorplans/racks/', views.FloorplanRackListView.as_view(), + name='floorplan_rack_list'), + path('floorplans/devices/', views.FloorplanDeviceListView.as_view(), + name='floorplan_device_list'), + path('floorplans/add/', views.FloorplanAddView.as_view(), name='floorplan_add'), + path('floorplans//edit/', + views.FloorplanMapEditView.as_view(), name='floorplan_edit'), + path('floorplans//delete/', + views.FloorplanDeleteView.as_view(), name='floorplan_delete'), + path('floorplans//changelog/', ObjectChangeLogView.as_view(), + name='floorplan_changelog', kwargs={'model': models.Floorplan}), + path('floorplans/floorplanimages/', + views.FloorplanImageListView.as_view(), name='floorplanimage_list'), + path('floorplans/floorplanimages/add/', + views.FloorplanImageEditView.as_view(), name='floorplanimage_add'), + path('floorplans/floorplanimages//edit/', + views.FloorplanImageEditView.as_view(), name='floorplanimage_edit'), + path('floorplans/floorplanimages//delete/', + views.FloorplanImageDeleteView.as_view(), name='floorplanimage_delete'), + path('floorplans/floorplanimages//', + views.FloorplanImageView.as_view(), name='floorplanimage'), + path('floorplans/floorplanimages//changelog/', ObjectChangeLogView.as_view(), + name='floorplanimage_changelog', kwargs={'model': models.FloorplanImage}), ) diff --git a/netbox_floorplan/utils.py b/netbox_floorplan/utils.py index a920180..76f377b 100644 --- a/netbox_floorplan/utils.py +++ b/netbox_floorplan/utils.py @@ -5,7 +5,6 @@ def file_upload(instance, filename): """ path = 'netbox-floorplan/' - if hasattr(instance, 'site'): - path_prepend = instance.site.id + return f'{path}_{filename}' + - return f'{path}{path_prepend}_{filename}' diff --git a/netbox_floorplan/views.py b/netbox_floorplan/views.py index 8a61071..92bca21 100644 --- a/netbox_floorplan/views.py +++ b/netbox_floorplan/views.py @@ -93,8 +93,10 @@ def get(self, request, pk): location = Location.objects.get(id=fp.location.id) racklist = Rack.objects.filter(site=site) form = forms.FloorplanRackFilterForm + form2 = forms.FloorplanForm return render(request, "netbox_floorplan/floorplan_edit.html", { "form": form, + "form2": form2, "site": site, "location": location, "racklist": racklist, @@ -133,3 +135,22 @@ def get(self, request): self.queryset = Device.objects.all().filter(~Q(id__in=fp_instance.mapped_devices)).filter( location=fp_instance.location.id).order_by("name") return super().get(request) + + +class FloorplanImageView(generic.ObjectView): + queryset = models.FloorplanImage.objects.all() + + +class FloorplanImageListView(generic.ObjectListView): + queryset = models.FloorplanImage.objects.all() + table = tables.FloorplanImageTable + + +class FloorplanImageEditView(generic.ObjectEditView): + queryset = models.FloorplanImage.objects.all() + form = forms.FloorplanImageForm + template_name = 'netbox_floorplan/floorplanimage_edit.html' + + +class FloorplanImageDeleteView(generic.ObjectDeleteView): + queryset = models.FloorplanImage.objects.all() From 97aa89f4c5067097537748c18023ab9e5f924070 Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Wed, 9 Oct 2024 23:17:33 +0100 Subject: [PATCH 02/12] bump versions --- netbox_floorplan/__init__.py | 8 ++++---- netbox_floorplan/version.py | 2 +- setup.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox_floorplan/__init__.py b/netbox_floorplan/__init__.py index 9ae55ab..6bd5eea 100644 --- a/netbox_floorplan/__init__.py +++ b/netbox_floorplan/__init__.py @@ -1,15 +1,15 @@ from netbox.plugins import PluginConfig - +from .version import __version__ class FloorplanConfig(PluginConfig): name = "netbox_floorplan" verbose_name = "Netbox Floorplan" description = "" - version = "0.4.1" + version = __version__ base_url = "floorplan" - min_version = "4.0.2" - max_version = "4.0.11" + min_version = "4.1.0" + max_version = "4.1.99" config = FloorplanConfig diff --git a/netbox_floorplan/version.py b/netbox_floorplan/version.py index 3d26edf..3d18726 100644 --- a/netbox_floorplan/version.py +++ b/netbox_floorplan/version.py @@ -1 +1 @@ -__version__ = "0.4.1" +__version__ = "0.5.0" diff --git a/setup.py b/setup.py index 63f3ff4..7ad2da5 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,6 @@ def get_version(rel_path): packages=find_packages(), include_package_data=True, zip_safe=False, - min_version="4.0.2", - max_version="4.0.11" + min_version="4.1.0", + max_version="4.1.99" ) From db0fe59e7947c9c48d141167633b5b30ab7006ec Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Wed, 9 Oct 2024 23:18:18 +0100 Subject: [PATCH 03/12] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index be04410..cd7f110 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ A netbox plugin providing floorplan mapping capability for locations and sites | 3.5 | >= 0.3.2 | | 3.6 | >= 0.3.2 | | 4.0.x | 0.4.1 | +| 4.1.x | 0.5.0 | ## Installing From 89125da5454d63b3e8462aff228f38b3aab958bf Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Thu, 10 Oct 2024 00:04:20 +0100 Subject: [PATCH 04/12] lint fixes --- netbox_floorplan/__init__.py | 1 + netbox_floorplan/api/serializers.py | 3 ++- netbox_floorplan/api/views.py | 1 + netbox_floorplan/forms.py | 7 ++++++- netbox_floorplan/models.py | 9 +++++---- netbox_floorplan/urls.py | 2 +- 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/netbox_floorplan/__init__.py b/netbox_floorplan/__init__.py index 6bd5eea..dc5f2ab 100644 --- a/netbox_floorplan/__init__.py +++ b/netbox_floorplan/__init__.py @@ -1,6 +1,7 @@ from netbox.plugins import PluginConfig from .version import __version__ + class FloorplanConfig(PluginConfig): name = "netbox_floorplan" diff --git a/netbox_floorplan/api/serializers.py b/netbox_floorplan/api/serializers.py index d38bd8c..aae96db 100644 --- a/netbox_floorplan/api/serializers.py +++ b/netbox_floorplan/api/serializers.py @@ -6,12 +6,13 @@ class FloorplanImageSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField( view_name='plugins-api:netbox_floorplan-api:floorplanimage-detail') - + class Meta: model = FloorplanImage fields = ['id', 'url', 'name', 'file', 'external_url', 'filename', 'comments', 'tags', 'custom_fields', 'created', 'last_updated'] brief_fields = ['id', 'url', 'name', 'file', 'filename', 'external_url'] + class FloorplanSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField( view_name='plugins-api:netbox_floorplan-api:floorplan-detail') diff --git a/netbox_floorplan/api/views.py b/netbox_floorplan/api/views.py index 7c0879e..318e7e6 100644 --- a/netbox_floorplan/api/views.py +++ b/netbox_floorplan/api/views.py @@ -9,6 +9,7 @@ class FloorplanViewSet(NetBoxModelViewSet): serializer_class = FloorplanSerializer filterset_class = filtersets.FloorplanFilterSet + class FloorplanImageViewSet(NetBoxModelViewSet): queryset = models.FloorplanImage.objects.prefetch_related('tags') serializer_class = FloorplanImageSerializer diff --git a/netbox_floorplan/forms.py b/netbox_floorplan/forms.py index 323f176..851b7ea 100644 --- a/netbox_floorplan/forms.py +++ b/netbox_floorplan/forms.py @@ -4,13 +4,16 @@ from utilities.forms.rendering import FieldSet from utilities.forms.fields import CommentField + class FloorplanImageForm(NetBoxModelForm): + comments = CommentField() fieldsets = ( FieldSet(('name', 'file', 'external_url', 'comments'), name='General'), - FieldSet(('comments', 'tags'), name=''), + FieldSet(('comments', 'tags'), name='') ) + class Meta: model = FloorplanImage fields = [ @@ -18,6 +21,8 @@ class Meta: 'file', 'external_url' ] + + class FloorplanForm(NetBoxModelForm): class Meta: model = Floorplan diff --git a/netbox_floorplan/models.py b/netbox_floorplan/models.py index 5a22722..c5ae279 100644 --- a/netbox_floorplan/models.py +++ b/netbox_floorplan/models.py @@ -21,7 +21,7 @@ class FloorplanImage(NetBoxModel): upload_to=file_upload, blank=True ) - + external_url = models.URLField( blank=True, max_length=255 @@ -36,7 +36,7 @@ def get_absolute_url(self): def __str__(self): return f'{self.name}' - + class Meta: ordering = ('name',) @@ -109,15 +109,17 @@ class Floorplan(NetBoxModel): assigned_image = models.ForeignKey( to='FloorplanImage', blank=True, - null=True, + null=True, on_delete=models.SET_NULL ) + width = models.DecimalField( max_digits=10, decimal_places=2, blank=True, null=True ) + height = models.DecimalField( max_digits=10, decimal_places=2, @@ -170,7 +172,6 @@ def mapped_racks(self): int(subobj["custom_meta"]["object_id"])) return drawn_racks - @property def mapped_devices(self): drawn_devices = [] diff --git a/netbox_floorplan/urls.py b/netbox_floorplan/urls.py index 6e48005..7cdf070 100644 --- a/netbox_floorplan/urls.py +++ b/netbox_floorplan/urls.py @@ -3,7 +3,7 @@ from netbox.views.generic import ObjectChangeLogView urlpatterns = ( - path('floorplans/', views.FloorplanListView.as_view(), + path('floorplans/', views.FloorplanListView.as_view(), name='floorplan_list'), path('floorplans/racks/', views.FloorplanRackListView.as_view(), name='floorplan_rack_list'), From 53e8fee332cfd2b6a09bac164ba5090ac879d034 Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Thu, 10 Oct 2024 00:11:54 +0100 Subject: [PATCH 05/12] flake8 --- .flake8 | 3 +++ netbox_floorplan/forms.py | 2 +- netbox_floorplan/urls.py | 36 ++++++++++++------------------------ netbox_floorplan/utils.py | 2 -- 4 files changed, 16 insertions(+), 27 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..eb33d99 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 140 +extend-ignore = E126, E722 \ No newline at end of file diff --git a/netbox_floorplan/forms.py b/netbox_floorplan/forms.py index 851b7ea..3af9c54 100644 --- a/netbox_floorplan/forms.py +++ b/netbox_floorplan/forms.py @@ -13,7 +13,7 @@ class FloorplanImageForm(NetBoxModelForm): FieldSet(('name', 'file', 'external_url', 'comments'), name='General'), FieldSet(('comments', 'tags'), name='') ) - + class Meta: model = FloorplanImage fields = [ diff --git a/netbox_floorplan/urls.py b/netbox_floorplan/urls.py index 7cdf070..49bb5c2 100644 --- a/netbox_floorplan/urls.py +++ b/netbox_floorplan/urls.py @@ -3,29 +3,17 @@ from netbox.views.generic import ObjectChangeLogView urlpatterns = ( - path('floorplans/', views.FloorplanListView.as_view(), - name='floorplan_list'), - path('floorplans/racks/', views.FloorplanRackListView.as_view(), - name='floorplan_rack_list'), - path('floorplans/devices/', views.FloorplanDeviceListView.as_view(), - name='floorplan_device_list'), + path('floorplans/', views.FloorplanListView.as_view(), name='floorplan_list'), + path('floorplans/racks/', views.FloorplanRackListView.as_view(), name='floorplan_rack_list'), + path('floorplans/devices/', views.FloorplanDeviceListView.as_view(), name='floorplan_device_list'), path('floorplans/add/', views.FloorplanAddView.as_view(), name='floorplan_add'), - path('floorplans//edit/', - views.FloorplanMapEditView.as_view(), name='floorplan_edit'), - path('floorplans//delete/', - views.FloorplanDeleteView.as_view(), name='floorplan_delete'), - path('floorplans//changelog/', ObjectChangeLogView.as_view(), - name='floorplan_changelog', kwargs={'model': models.Floorplan}), - path('floorplans/floorplanimages/', - views.FloorplanImageListView.as_view(), name='floorplanimage_list'), - path('floorplans/floorplanimages/add/', - views.FloorplanImageEditView.as_view(), name='floorplanimage_add'), - path('floorplans/floorplanimages//edit/', - views.FloorplanImageEditView.as_view(), name='floorplanimage_edit'), - path('floorplans/floorplanimages//delete/', - views.FloorplanImageDeleteView.as_view(), name='floorplanimage_delete'), - path('floorplans/floorplanimages//', - views.FloorplanImageView.as_view(), name='floorplanimage'), - path('floorplans/floorplanimages//changelog/', ObjectChangeLogView.as_view(), - name='floorplanimage_changelog', kwargs={'model': models.FloorplanImage}), + path('floorplans//edit/', views.FloorplanMapEditView.as_view(), name='floorplan_edit'), + path('floorplans//delete/', views.FloorplanDeleteView.as_view(), name='floorplan_delete'), + path('floorplans//changelog/', ObjectChangeLogView.as_view(), name='floorplan_changelog', kwargs={'model': models.Floorplan}), + path('floorplans/floorplanimages/', views.FloorplanImageListView.as_view(), name='floorplanimage_list'), + path('floorplans/floorplanimages/add/', views.FloorplanImageEditView.as_view(), name='floorplanimage_add'), + path('floorplans/floorplanimages//edit/', views.FloorplanImageEditView.as_view(), name='floorplanimage_edit'), + path('floorplans/floorplanimages//delete/', views.FloorplanImageDeleteView.as_view(), name='floorplanimage_delete'), + path('floorplans/floorplanimages//', views.FloorplanImageView.as_view(), name='floorplanimage'), + path('floorplans/floorplanimages//changelog/', ObjectChangeLogView.as_view(), name='floorplanimage_changelog', kwargs={'model': models.FloorplanImage}), ) diff --git a/netbox_floorplan/utils.py b/netbox_floorplan/utils.py index 76f377b..69150ce 100644 --- a/netbox_floorplan/utils.py +++ b/netbox_floorplan/utils.py @@ -6,5 +6,3 @@ def file_upload(instance, filename): path = 'netbox-floorplan/' return f'{path}_{filename}' - - From df34ff659636a7fa5cb6774f70ad132d2ba2f06b Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Thu, 10 Oct 2024 00:14:20 +0100 Subject: [PATCH 06/12] flake8 --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index eb33d99..69f4fb2 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] max-line-length = 140 -extend-ignore = E126, E722 \ No newline at end of file +ignore = E126, E722 \ No newline at end of file From 9aeb3de7c4918cb3e86edb6dbc19bc7e429a9f29 Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Thu, 10 Oct 2024 01:40:00 +0100 Subject: [PATCH 07/12] tweak image location --- .../static/netbox_floorplan/floorplan/edit.js | 49 ++++++++++++++++--- .../netbox_floorplan/floorplan/utils.js | 47 +++++++++++++++--- .../netbox_floorplan/vendors/htmx.min.js | 1 + .../netbox_floorplan/floorplan_edit.html | 4 +- 4 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 netbox_floorplan/static/netbox_floorplan/vendors/htmx.min.js diff --git a/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js b/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js index 7b3943f..98ce64a 100644 --- a/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js +++ b/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js @@ -481,15 +481,47 @@ function update_background() { } var img = fabric.Image.fromURL(img_url, function(img) { - let scaleRatio = Math.max(canvas.width / img.width, canvas.height / img.height); - canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { - scaleX: scaleRatio, - scaleY: scaleRatio, - left: canvas.width / 2, - top: canvas.height / 2, - originX: 'middle', - originY: 'middle' + + + var left = 0; + var top = 0; + var width = 0; + var height = 0; + canvas.getObjects().forEach(function (object) { + if (object.custom_meta) { + if (object.custom_meta.object_type == "floorplan_boundry") { + alert("found") + left = object.left; + top = object.top; + width = object.width; + height = object.height; + } + } }); + alert(height + ":" + width) + // if we have a floorplan boundary, position the image in there + if (height != 0 && width != 0) { + let scaleRatioX = Math.max(width / img.width) + let scaleRatioY = Math.max(height / img.height); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { + scaleX: scaleRatioX, + scaleY: scaleRatioY, + left: left, + top: top + }); + } + else + { + let scaleRatio = Math.max(canvas.width / img.width, canvas.height / img.height); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { + scaleX: scaleRatio, + scaleY: scaleRatio, + left: canvas.width / 2, + top: canvas.height / 2, + originX: 'middle', + originY: 'middle' + }); + } }); } else { @@ -571,6 +603,7 @@ function update_dimensions() { centeredRotation: true, }); + var text = new fabric.IText(`${obj_name}`, { fontFamily: "Courier New", fontSize: 16, diff --git a/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js b/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js index e61116e..142cb82 100644 --- a/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js +++ b/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js @@ -161,16 +161,47 @@ function init_floor_plan(floorplan_id, canvas, mode) { img_url = floorplan.assigned_image.file; } + var img = fabric.Image.fromURL(img_url, function(img) { - let scaleRatio = Math.max(canvas.width / img.width, canvas.height / img.height); - canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { - scaleX: scaleRatio, - scaleY: scaleRatio, - left: canvas.width / 2, - top: canvas.height / 2, - originX: 'middle', - originY: 'middle' + var left = 0; + var top = 0; + var width = 0; + var height = 0; + canvas.getObjects().forEach(function (object) { + if (object.custom_meta) { + if (object.custom_meta.object_type == "floorplan_boundry") { + alert("found") + left = object.left; + top = object.top; + width = object.width; + height = object.height; + } + } }); + alert(height + ":" + width) + // if we have a floorplan boundary, position the image in there + if (height != 0 && width != 0) { + let scaleRatioX = Math.max(width / img.width) + let scaleRatioY = Math.max(height / img.height); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { + scaleX: scaleRatioX, + scaleY: scaleRatioY, + left: left, + top: top + }); + } + else + { + let scaleRatio = Math.max(canvas.width / img.width, canvas.height / img.height); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { + scaleX: scaleRatio, + scaleY: scaleRatio, + left: canvas.width / 2, + top: canvas.height / 2, + originX: 'middle', + originY: 'middle' + }); + } }); diff --git a/netbox_floorplan/static/netbox_floorplan/vendors/htmx.min.js b/netbox_floorplan/static/netbox_floorplan/vendors/htmx.min.js new file mode 100644 index 0000000..de5f0f1 --- /dev/null +++ b/netbox_floorplan/static/netbox_floorplan/vendors/htmx.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.12"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t){return new RegExp("<"+e+"(\\s[^>]*>|>)([\\s\\S]*?)<\\/"+e+">",!!t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function s(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/",0);var a=i.querySelector("template").content;if(Q.config.allowScriptTags){oe(a.querySelectorAll("script"),function(e){if(Q.config.inlineScriptNonce){e.nonce=Q.config.inlineScriptNonce}e.htmxExecuted=navigator.userAgent.indexOf("Firefox")===-1})}else{oe(a.querySelectorAll("script"),function(e){_(e)})}return a}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return s(""+n+"
",1);case"col":return s(""+n+"
",2);case"tr":return s(""+n+"
",2);case"td":case"th":return s(""+n+"
",3);case"script":case"style":return s("
"+n+"
",1);default:return s(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function D(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){b(e);return null}}function U(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function B(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function t(e){return Tr(re().body,function(){return eval(e)})}function F(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function j(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function _(e,t){e=p(e);if(t){setTimeout(function(){_(e);e=null},t)}else{e.parentElement.removeChild(e)}}function z(e,t,r){e=p(e);if(r){setTimeout(function(){z(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=p(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function $(e,t){e=p(e);e.classList.toggle(t)}function W(e,t){e=p(e);oe(e.parentElement.children,function(e){n(e,t)});z(e,t)}function v(e,t){e=p(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function G(e,t){return e.substring(e.length-t.length)===t}function J(e){var t=e.trim();if(g(t,"<")&&G(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function Z(e,t){if(t.indexOf("closest ")===0){return[v(e,J(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,J(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[K(e,J(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[Y(e,J(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(J(t))}}var K=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return Z(e,t)[0]}else{return Z(re().body,e)[0]}}function p(e){if(I(e,"String")){return C(e)}else{return e}}function ve(e,t,r){if(k(t)){return{target:re().body,event:e,listener:t}}else{return{target:p(e),event:t,listener:r}}}function de(t,r,n){jr(function(){var e=ve(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=k(r);return e?r:n}function ge(t,r,n){jr(function(){var e=ve(t,r,n);e.target.removeEventListener(e.event,e.listener)});return k(r)?r:n}var pe=re().createElement("output");function me(e,t){var r=ne(e,t);if(r){if(r==="this"){return[xe(e,t)]}else{var n=Z(e,r);if(n.length===0){b('The selector "'+r+'" on '+t+" returned no matches!");return[pe]}else{return n}}}}function xe(e,t){return c(e,function(e){return te(e,t)!=null})}function ye(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return xe(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function be(e){var t=Q.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!Se(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Fe(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function Ce(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(var a=0;a0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();we(e,i);s.tasks.push(function(){we(e,a)})}}})}function Oe(e){return function(){n(e,Q.config.addedClass);zt(e);Nt(e);qe(e);ce(e,"htmx:load")}}function qe(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Te(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;z(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Oe(i))}}}function He(e,t){var r=0;while(r-1){var t=e.replace(H,"");var r=t.match(q);if(r){return r[2]}}}function je(e,t,r,n,i,a){i.title=Ve(n);var o=l(n);if(o){Ce(r,o,i);o=Be(r,o,a);Re(o);return Fe(e,r,t,o,i)}}function _e(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=E(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!P(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=Tr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){fe(re().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Qe(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function y(e,t){var r="";while(e.length>0&&!t.test(e[0])){r+=e.shift()}return r}function tt(e){var t;if(e.length>0&&Ze.test(e[0])){e.shift();t=y(e,Ke).trim();e.shift()}else{t=y(e,x)}return t}var rt="input, textarea, select";function nt(e,t,r){var n=[];var i=Ye(t);do{y(i,Je);var a=i.length;var o=y(i,/[,\[\s]/);if(o!==""){if(o==="every"){var s={trigger:"every"};y(i,Je);s.pollInterval=d(y(i,/[,\[\s]/));y(i,Je);var l=et(e,i,"event");if(l){s.eventFilter=l}n.push(s)}else if(o.indexOf("sse:")===0){n.push({trigger:"sse",sseEvent:o.substr(4)})}else{var u={trigger:o};var l=et(e,i,"event");if(l){u.eventFilter=l}while(i.length>0&&i[0]!==","){y(i,Je);var f=i.shift();if(f==="changed"){u.changed=true}else if(f==="once"){u.once=true}else if(f==="consume"){u.consume=true}else if(f==="delay"&&i[0]===":"){i.shift();u.delay=d(y(i,x))}else if(f==="from"&&i[0]===":"){i.shift();if(Ze.test(i[0])){var c=tt(i)}else{var c=y(i,x);if(c==="closest"||c==="find"||c==="next"||c==="previous"){i.shift();var h=tt(i);if(h.length>0){c+=" "+h}}}u.from=c}else if(f==="target"&&i[0]===":"){i.shift();u.target=tt(i)}else if(f==="throttle"&&i[0]===":"){i.shift();u.throttle=d(y(i,x))}else if(f==="queue"&&i[0]===":"){i.shift();u.queue=y(i,x)}else if(f==="root"&&i[0]===":"){i.shift();u[f]=tt(i)}else if(f==="threshold"&&i[0]===":"){i.shift();u[f]=y(i,x)}else{fe(e,"htmx:syntax:error",{token:i.shift()})}}n.push(u)}}if(i.length===a){fe(e,"htmx:syntax:error",{token:i.shift()})}y(i,Je)}while(i[0]===","&&i.shift());if(r){r[t]=n}return n}function it(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Q.config.triggerSpecsCache;r=n&&n[t]||nt(e,t,n)}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,rt)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function at(e){ae(e).cancelled=true}function ot(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ct(r,e,Wt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}ot(e,t,r)}},r.pollInterval)}function st(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function lt(t,r,e){if(t.tagName==="A"&&st(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=ee(t,"href")}else{var a=ee(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=ee(t,"action")}e.forEach(function(e){ht(t,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(n,i,e,t)},r,e,true)})}}function ut(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ft(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ct(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){fe(re().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function ht(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=Z(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ae(e);t.lastValue=e.value})}oe(t,function(n){var i=function(e){if(!se(a)){n.removeEventListener(s.trigger,i);return}if(ft(a,e)){return}if(l||ut(e,a)){e.preventDefault()}if(ct(s,a,e)){return}var t=ae(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ae(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle>0){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay>0){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{ce(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var vt=false;var dt=null;function gt(){if(!dt){dt=function(){vt=true};window.addEventListener("scroll",dt);setInterval(function(){if(vt){vt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){pt(e)})}},200)}}function pt(t){if(!o(t,"data-hx-revealed")&&X(t)){t.setAttribute("data-hx-revealed","true");var e=ae(t);if(e.initHash){ce(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ce(t,"revealed")},{once:true})}}}function mt(e,t,r){var n=D(r);for(var i=0;i=0){var t=wt(n);setTimeout(function(){xt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(yt(s)){return}var t=e.data;R(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=M(n.children);for(var a=0;a0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(ut(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function wt(e){var t=Q.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}b('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function St(e,t,r){var n=D(r);for(var i=0;i0){setTimeout(i,n)}else{i()}}function Ht(t,i,e){var a=false;oe(w,function(r){if(o(t,"hx-"+r)){var n=te(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){Lt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(r,n,e,t)})})}});return a}function Lt(n,e,t,r){if(e.sseEvent){Rt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){gt();ht(n,r,t,e);pt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=ue(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t0){t.polling=true;ot(n,r,e)}else{ht(n,r,t,e)}}function At(e){if(!e.htmxExecuted&&Q.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=re().createElement("script");oe(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){b(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Nt(e){if(h(e,"script")){At(e)}oe(f(e,"script"),function(e){At(e)})}function It(e){var t=e.attributes;if(!t){return false}for(var r=0;r0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Bt(o)}for(var l in r){Ft(e,l,r[l])}}}function jt(e){Ae(e);for(var t=0;tQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(re().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Yt(e){if(!U()){return null}e=B(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){ce(re().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Zt();var r=T(t);var n=Ve(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ue(t,e,r);nr(r.tasks);Jt=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function ar(e){er();e=e||location.pathname+location.search;var t=Yt(e);if(t){var r=l(t.content);var n=Zt();var i=T(n);Ue(n,r,i);nr(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Jt=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{ir(e)}}}function or(e){var t=me(e,"hx-indicator");if(t==null){t=[e]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Q.config.requestClass)});return t}function sr(e){var t=me(e,"hx-disabled-elt");if(t==null){t=[]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function lr(e,t){oe(e,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Q.config.requestClass)}});oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function ur(e,t){for(var r=0;r=0}function wr(e,t){var r=t?t:ne(e,"hx-swap");var n={swapStyle:ae(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ae(e).boosted&&!br(e)){n["show"]="top"}if(r){var i=D(r);if(i.length>0){for(var a=0;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{b("Unknown modifier in hx-swap: "+o)}}}}return n}function Sr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function Er(t,r,n){var i=null;R(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(Sr(r)){return mr(n)}else{return pr(n)}}}function T(e){return{tasks:[],elts:[e]}}function Cr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ue(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ue(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Rr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=te(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=Tr(e,function(){return Function("return ("+a+")")()},{})}else{s=E(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Rr(u(e),t,r,n)}function Tr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Or(e,t){return Rr(e,"hx-vars",true,t)}function qr(e,t){return Rr(e,"hx-vals",false,t)}function Hr(e){return le(Or(e),qr(e))}function Lr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Ar(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(re().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Nr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||I(r,"String")){return he(e,t,null,null,{targetOverride:p(r),returnPromise:true})}else{return he(e,t,p(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:p(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Ir(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function kr(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!n){return false}}return ce(e,"htmx:validateUrl",le({url:i,sameHost:n},r))}function he(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=re().body}var M=a.handler||Mr;var X=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ye(n);if(u==null||u==pe){fe(n,"htmx:targetError",{target:te(n,"hx-target")});ie(s);return l}var f=ae(n);var c=f.lastButtonClicked;if(c){var h=ee(c,"formaction");if(h!=null){r=h}var v=ee(c,"formmethod");if(v!=null){if(v.toLowerCase()!=="dialog"){t=v}}}var d=ne(n,"hx-confirm");if(e===undefined){var D=function(e){return he(t,r,n,i,a,!!e)};var U={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var p=ne(n,"hx-sync");var m=null;var x=false;if(p){var B=p.split(":");var F=B[0].trim();if(F==="this"){g=xe(n,"hx-sync")}else{g=ue(n,F)}p=(B[1]||"drop").trim();f=ae(g);if(p==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(p==="abort"){if(f.xhr){ie(o);return l}else{x=true}}else if(p==="replace"){ce(g,"htmx:abort")}else if(p.indexOf("queue")===0){var V=p.split(" ");m=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(m==null){if(i){var y=ae(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){m=y.triggerSpec.queue}}if(m==null){m="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(m==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){he(t,r,n,i,a)})}ie(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var j=ne(n,"hx-prompt");if(j){var S=prompt(j);if(S===null||!ce(n,"htmx:prompt",{prompt:S,target:u})){ie(o);w();return l}}if(d&&!e){if(!confirm(d)){ie(o);w();return l}}var E=xr(n,u,S);if(t!=="get"&&!Sr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=dr(n,t);var C=_.errors;var R=_.values;if(a.values){R=le(R,a.values)}var z=Hr(n);var $=le(R,z);var T=yr($,n);if(Q.config.getCacheBusterParam&&t==="get"){T["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=Rr(n,"hx-request");var W=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:T,unfilteredParameters:$,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||O.credentials||Q.config.withCredentials,timeout:a.timeout||O.timeout||Q.config.timeout,path:r,triggeringEvent:i};if(!ce(n,"htmx:configRequest",H)){ie(o);w();return l}r=H.path;t=H.verb;E=H.headers;T=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){ce(n,"htmx:validation:halted",H);ie(o);w();return l}var G=r.split("#");var J=G[0];var L=G[1];var A=r;if(q){A=J;var Z=Object.keys(T).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=pr(T);if(L){A+="#"+L}}}if(!kr(n,A,H)){fe(n,"htmx:invalidPath",H);ie(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var K=E[N];Lr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,select:X,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Ir(n);I.pathInfo.responsePath=Ar(b);M(n,I);lr(k,P);ce(n,"htmx:afterRequest",I);ce(n,"htmx:afterOnLoad",I);if(!se(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(se(r)){t=r}}if(t){ce(t,"htmx:afterRequest",I);ce(t,"htmx:afterOnLoad",I)}}ie(o);w()}catch(e){fe(n,"htmx:onLoadError",le({error:e},I));throw e}};b.onerror=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:timeout",I);ie(s);w()};if(!ce(n,"htmx:beforeRequest",I)){ie(o);w();return l}var k=or(n);var P=sr(n);oe(["loadstart","loadend","progress","abort"],function(t){oe([b,b.upload],function(e){e.addEventListener(t,function(e){ce(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ce(n,"htmx:beforeSend",I);var Y=q?null:Er(b,n,T);b.send(Y);return l}function Pr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=ne(e,"hx-push-url");var l=ne(e,"hx-replace-url");var u=ae(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Mr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;var h=u.select;if(!ce(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){_e(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){er();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Nr("GET",r,v).then(function(){tr(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){if(f.getResponseHeader("HX-Retarget")==="this"){u.target=l}else{u.target=ue(l,f.getResponseHeader("HX-Retarget"))}}var d=Pr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var p=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:p},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;p=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){at(l)}R(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){er()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=wr(l,s);if(v.hasOwnProperty("ignoreTitle")){p=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var m=null;var x=null;var y=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(h){r=h}if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}if(d.type){ce(re().body,"htmx:beforeHistoryUpdate",le({history:d},u));if(d.type==="push"){tr(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{rr(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=T(c);je(v.swapStyle,c,l,g,n,r);if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){var i=document.getElementById(ee(t.elt,"id"));var a={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!Q.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Q.config.swappingClass);oe(n.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ce(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!se(l)){o=re().body}_e(f,"HX-Trigger-After-Swap",o)}var s=function(){oe(n.tasks,function(e){e.call()});oe(n.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ce(e,"htmx:afterSettle",u)});if(u.pathInfo.anchor){var e=re().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!p){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}Cr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}_e(f,"HX-Trigger-After-Settle",r)}ie(m)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(x);throw e}};var b=Q.config.globalViewTransitions;if(v.hasOwnProperty("transition")){b=v.transition}if(b&&ce(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var w=new Promise(function(e,t){m=e;x=t});var S=y;y=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(y,v.swapDelay)}else{y()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Xr={};function Dr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ur(e,t){if(t.init){t.init(r)}Xr[e]=le(Dr(),t)}function Br(e){delete Xr[e]}function Fr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=te(e,"hx-ext");if(t){oe(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Xr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Fr(u(e),r,n)}var Vr=false;re().addEventListener("DOMContentLoaded",function(){Vr=true});function jr(e){if(Vr||re().readyState==="complete"){e()}else{re().addEventListener("DOMContentLoaded",e)}}function _r(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","")}}function zr(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function $r(){var e=zr();if(e){Q.config=le(Q.config,e)}}jr(function(){$r();_r();var e=re().body;zt(e);var t=re().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ae(t);if(r&&r.xhr){r.xhr.abort()}});const r=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){ar();oe(t,function(e){ce(e,"htmx:restored",{document:re(),triggerEvent:ce})})}else{if(r){r(e)}}};setTimeout(function(){ce(e,"htmx:load",{});e=null},0)});return Q}()}); \ No newline at end of file diff --git a/netbox_floorplan/templates/netbox_floorplan/floorplan_edit.html b/netbox_floorplan/templates/netbox_floorplan/floorplan_edit.html index 904160a..d70ad77 100644 --- a/netbox_floorplan/templates/netbox_floorplan/floorplan_edit.html +++ b/netbox_floorplan/templates/netbox_floorplan/floorplan_edit.html @@ -8,7 +8,7 @@ {{ block.super }} - + {% endblock head %} @@ -251,5 +251,5 @@