├── .appcast.xml ├── .gitignore ├── Alembic.sketchplugin └── Contents │ ├── Resources │ ├── alembic.html │ ├── assets │ │ ├── css │ │ │ └── style.css │ │ └── js │ │ │ ├── color-thief.min.js │ │ │ └── main.js │ ├── icon@2x.png │ └── runner@2x.png │ └── Sketch │ ├── MochaJSDelegate.js │ ├── alembic.js │ └── manifest.json ├── Docs ├── alembic.gif ├── download.png ├── icon.png ├── runnerBadge.png ├── runnerInstall.png ├── runnerUsage.gif └── usage.gif ├── LICENSE └── README.md /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Alembic 5 | https://github.com/awkward/Alembic 6 | Extract colors from your images. 7 | en 8 | 9 | Alembic 1.0 10 | 11 | 12 | 13 | Alembic 1.0.1 14 | 15 | 16 | 17 | Alembic 1.1 18 | 19 | 20 | 21 | Alembic 1.1.1 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | assets/.DS_Store 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /Alembic.sketchplugin/Contents/Resources/alembic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Alembic 6 | 7 | 8 | 9 |
10 | 16 | 25 |
26 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Alembic.sketchplugin/Contents/Resources/assets/css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | overflow: hidden; 4 | } 5 | 6 | *, *:before, *:after { 7 | box-sizing: inherit; 8 | margin: 0; 9 | padding: 0; 10 | position: relative; 11 | } 12 | 13 | body { 14 | font-family: -apple-system, sans-serif; 15 | user-select: none; 16 | -webkit-user-select: none; 17 | padding: 0 6px 6px 6px; 18 | } 19 | 20 | .hiddenImage { 21 | display: none; 22 | } 23 | 24 | .hidden, input { 25 | opacity: 0; 26 | pointer-events: none; 27 | } 28 | 29 | .container { 30 | width: 100%; 31 | height: 120px; 32 | } 33 | 34 | .imagePreview { 35 | position: absolute; 36 | width: 100%; 37 | height: 120px; 38 | border-radius: 2px; 39 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); 40 | background-position: center; 41 | background-repeat: no-repeat; 42 | background-size: cover; 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | transition: opacity .2s ease-in-out; 47 | } 48 | 49 | .imagePreview__toast { 50 | display: flex; 51 | flex-direction: column; 52 | color: #fff; 53 | font-size: 13px; 54 | background: rgba(0, 0, 0, 0.6); 55 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 4px 8px rgba(0, 0, 0, 0.2); 56 | -webkit-backdrop-filter: saturate(180%) blur(20px); 57 | backdrop-filter: saturate(180%) blur(20px); 58 | border-radius: 5px; 59 | transition: opacity .2s ease-in-out; 60 | } 61 | 62 | .imagePreview__toast div { 63 | display: flex; 64 | align-items: center; 65 | padding: 8px 10px; 66 | } 67 | 68 | .imagePreview__toast span { 69 | display: inline-block; 70 | background: red; 71 | width: 14px; 72 | height: 14px; 73 | border-radius: 2px; 74 | box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3); 75 | margin-right: 8px; 76 | } 77 | 78 | .imagePreview__toast a { 79 | background: transparent; 80 | cursor: pointer; 81 | color: inherit; 82 | font-weight: 700; 83 | text-decoration: none; 84 | text-align: center; 85 | padding: 0 10px; 86 | height: 32px; 87 | line-height: 32px; 88 | overflow: hidden; 89 | border-bottom-right-radius: 5px; 90 | border-bottom-left-radius: 5px; 91 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); 92 | transition: all 0.2s ease-in-out; 93 | } 94 | 95 | .imagePreview__toast a:hover { 96 | background: rgba(255, 255, 255, 0.2); 97 | } 98 | 99 | .imagePreview__toast a:active { 100 | background: rgba(255, 255, 255, 0); 101 | } 102 | 103 | .imagePreview__toast a.collapsed { 104 | color: transparent; 105 | height: 0; 106 | } 107 | 108 | .emptyContainer { 109 | position: absolute; 110 | width: 100%; 111 | height: 120px; 112 | display: flex; 113 | justify-content: center; 114 | align-items: center; 115 | transition: opacity .2s ease-in-out; 116 | } 117 | 118 | .empty { 119 | text-align: center; 120 | } 121 | 122 | .empty h3 { 123 | font-size: 13px; 124 | font-weight: 700; 125 | } 126 | 127 | .empty p { 128 | margin-top: 5px; 129 | font-size: 12px; 130 | color: rgba(0, 0, 0, 0.5); 131 | font-weight: normal; 132 | } 133 | 134 | .colorPalette { 135 | display: flex; 136 | flex-flow: row wrap; 137 | list-style: none; 138 | margin-top: 6px; 139 | transition: opacity .2s ease-in-out; 140 | } 141 | 142 | .colorPalette__color { 143 | background: transparent; 144 | width: 42px; 145 | height: 42px; 146 | margin-right: 6px; 147 | border-radius: 2px; 148 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); 149 | cursor: pointer; 150 | display: flex; 151 | padding-top: 10px; 152 | justify-content: center; 153 | transition: transform .1s ease-in-out, background-color .3s ease-in-out; 154 | } 155 | 156 | .colorPalette__color svg { 157 | flex: none; 158 | opacity: 0; 159 | } 160 | 161 | .colorPalette__color:last-of-type { 162 | margin-right: 0; 163 | } 164 | 165 | .colorPalette__color:hover svg { 166 | opacity: 1; 167 | } 168 | 169 | .colorPalette__color:active { 170 | transform: scale(0.9); 171 | } 172 | -------------------------------------------------------------------------------- /Alembic.sketchplugin/Contents/Resources/assets/js/color-thief.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Color Thief v2.0 3 | * by Lokesh Dhakar - http://www.lokeshdhakar.com 4 | * 5 | * Thanks 6 | * ------ 7 | * Nick Rabinowitz - For creating quantize.js. 8 | * John Schulz - For clean up and optimization. @JFSIII 9 | * Nathan Spady - For adding drag and drop support to the demo page. 10 | * 11 | * License 12 | * ------- 13 | * Copyright 2011, 2015 Lokesh Dhakar 14 | * Released under the MIT license 15 | * https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE 16 | * 17 | * @license 18 | */ 19 | var CanvasImage=function(a){this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),document.body.appendChild(this.canvas),this.width=this.canvas.width=a.width,this.height=this.canvas.height=a.height,this.context.drawImage(a,0,0,this.width,this.height)};CanvasImage.prototype.clear=function(){this.context.clearRect(0,0,this.width,this.height)},CanvasImage.prototype.update=function(a){this.context.putImageData(a,0,0)},CanvasImage.prototype.getPixelCount=function(){return this.width*this.height},CanvasImage.prototype.getImageData=function(){return this.context.getImageData(0,0,this.width,this.height)},CanvasImage.prototype.removeCanvas=function(){this.canvas.parentNode.removeChild(this.canvas)};var ColorThief=function(){};/*! 20 | * quantize.js Copyright 2008 Nick Rabinowitz. 21 | * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 22 | * @license 23 | */ 24 | /*! 25 | * Block below copied from Protovis: http://mbostock.github.com/protovis/ 26 | * Copyright 2010 Stanford Visualization Group 27 | * Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php 28 | * @license 29 | */ 30 | if(ColorThief.prototype.getColor=function(a,b){var c=this.getPalette(a,5,b),d=c[0];return d},ColorThief.prototype.getPalette=function(a,b,c){"undefined"==typeof b&&(b=10),("undefined"==typeof c||c<1)&&(c=10);for(var d,e,f,g,h,i=new CanvasImage(a),j=i.getImageData(),k=j.data,l=i.getPixelCount(),m=[],n=0;n=125&&(e>250&&f>250&&g>250||m.push([e,f,g]));var o=MMCQ.quantize(m,b),p=o?o.palette():null;return i.removeCanvas(),p},!pv)var pv={map:function(a,b){var c={};return b?a.map(function(a,d){return c.index=d,b.call(c,a)}):a.slice()},naturalOrder:function(a,b){return ab?1:0},sum:function(a,b){var c={};return a.reduce(b?function(a,d,e){return c.index=e,a+b.call(c,d)}:function(a,b){return a+b},0)},max:function(a,b){return Math.max.apply(null,b?pv.map(a,b):a)}};var MMCQ=function(){function a(a,b,c){return(a<<2*i)+(b<>j,e=b[1]>>j,f=b[2]>>j,c=a(d,e,f),h[c]=(h[c]||0)+1}),h}function f(a,b){var d,e,f,g=1e6,h=0,i=1e6,k=0,l=1e6,m=0;return a.forEach(function(a){d=a[0]>>j,e=a[1]>>j,f=a[2]>>j,dh&&(h=d),ek&&(k=e),fm&&(m=f)}),new c(g,h,i,k,l,m,b)}function g(b,c){function d(a){var b,d,e,f,g,h=a+"1",j=a+"2",k=0;for(i=c[h];i<=c[j];i++)if(o[i]>n/2){for(e=c.copy(),f=c.copy(),b=i-c[h],d=c[j]-i,g=b<=d?Math.min(c[j]-1,~~(i+d/2)):Math.max(c[h],~~(i-1-b/2));!o[g];)g++;for(k=p[g];!k&&o[g-1];)k=p[--g];return e[j]=g,f[h]=e[j]+1,[e,f]}}if(c.count()){var e=c.r2-c.r1+1,f=c.g2-c.g1+1,g=c.b2-c.b1+1,h=pv.max([e,f,g]);if(1==c.count())return[c.copy()];var i,j,k,l,m,n=0,o=[],p=[];if(h==e)for(i=c.r1;i<=c.r2;i++){for(l=0,j=c.g1;j<=c.g2;j++)for(k=c.b1;k<=c.b2;k++)m=a(i,j,k),l+=b[m]||0;n+=l,o[i]=n}else if(h==f)for(i=c.g1;i<=c.g2;i++){for(l=0,j=c.r1;j<=c.r2;j++)for(k=c.b1;k<=c.b2;k++)m=a(j,i,k),l+=b[m]||0;n+=l,o[i]=n}else for(i=c.b1;i<=c.b2;i++){for(l=0,j=c.r1;j<=c.r2;j++)for(k=c.g1;k<=c.g2;k++)m=a(j,k,i),l+=b[m]||0;n+=l,o[i]=n}return o.forEach(function(a,b){p[b]=n-a}),d(h==e?"r":h==f?"g":"b")}}function h(a,c){function h(a,b){for(var c,d=1,e=0;e=b)return;if(e++>k)return}else a.push(c),e++}if(!a.length||c<2||c>256)return!1;var i=e(a),j=0;i.forEach(function(){j++});var m=f(a,i),n=new b(function(a,b){return pv.naturalOrder(a.count(),b.count())});n.push(m),h(n,l*c);for(var o=new b(function(a,b){return pv.naturalOrder(a.count()*a.volume(),b.count()*b.volume())});n.size();)o.push(n.pop());h(o,c-o.size());for(var p=new d;o.size();)p.push(o.pop());return p}var i=5,j=8-i,k=1e3,l=.75;return c.prototype={volume:function(a){var b=this;return b._volume&&!a||(b._volume=(b.r2-b.r1+1)*(b.g2-b.g1+1)*(b.b2-b.b1+1)),b._volume},count:function(b){var c=this,d=c.histo;if(!c._count_set||b){var e,f,g,h=0;for(e=c.r1;e<=c.r2;e++)for(f=c.g1;f<=c.g2;f++)for(g=c.b1;g<=c.b2;g++)index=a(e,f,g),h+=d[index]||0;c._count=h,c._count_set=!0}return c._count},copy:function(){var a=this;return new c(a.r1,a.r2,a.g1,a.g2,a.b1,a.b2,a.histo)},avg:function(b){var c=this,d=c.histo;if(!c._avg||b){var e,f,g,h,j,k=0,l=1<<8-i,m=0,n=0,o=0;for(f=c.r1;f<=c.r2;f++)for(g=c.g1;g<=c.g2;g++)for(h=c.b1;h<=c.b2;h++)j=a(f,g,h),e=d[j]||0,k+=e,m+=e*(f+.5)*l,n+=e*(g+.5)*l,o+=e*(h+.5)*l;k?c._avg=[~~(m/k),~~(n/k),~~(o/k)]:c._avg=[~~(l*(c.r1+c.r2+1)/2),~~(l*(c.g1+c.g2+1)/2),~~(l*(c.b1+c.b2+1)/2)]}return c._avg},contains:function(a){var b=this,c=a[0]>>j;return gval=a[1]>>j,bval=a[2]>>j,c>=b.r1&&c<=b.r2&&gval>=b.g1&&gval<=b.g2&&bval>=b.b1&&bval<=b.b2}},d.prototype={push:function(a){this.vboxes.push({vbox:a,color:a.avg()})},palette:function(){return this.vboxes.map(function(a){return a.color})},size:function(){return this.vboxes.size()},map:function(a){for(var b=this.vboxes,c=0;c251&&d[1]>251&&d[2]>251&&(a[c].color=[255,255,255])}},{quantize:h}}(); -------------------------------------------------------------------------------- /Alembic.sketchplugin/Contents/Resources/assets/js/main.js: -------------------------------------------------------------------------------- 1 | function toHex(rgb) { 2 | return "#" + ("0" + parseInt(rgb[0], 10).toString(16)).slice(-2) + ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2); 3 | } 4 | 5 | function lerp(value, fromLow, fromHigh, toLow, toHigh) { 6 | return toLow + (((value - fromLow) / (fromHigh - fromLow)) * (toHigh - toLow)) 7 | } 8 | 9 | function toSecondaryColor(rgb) { 10 | var brightness = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; 11 | var difference = (brightness < 128) ? lerp(brightness, 10, 128, 160, 100) : lerp(brightness, 128, 246, -100, -160); 12 | 13 | for (var i = 0; i < rgb.length; i++) { 14 | rgb[i] += difference; 15 | rgb[i] = rgb[i] < 0 ? 0 : rgb[i] > 255 ? 255 : rgb[i] | 0; 16 | } 17 | 18 | return rgb; 19 | } 20 | 21 | function emptyState() { 22 | document.querySelector(".emptyContainer").classList.remove("hidden"); 23 | document.querySelector(".imagePreview").classList.add("hidden"); 24 | document.querySelector(".colorPalette").classList.add("hidden"); 25 | 26 | var colors = document.querySelectorAll(".colorPalette__color"); 27 | for (var i = 0; i < colors.length; i++) { 28 | colors[i].style.backgroundColor = "transparent"; 29 | } 30 | } 31 | 32 | function update(base64) { 33 | var colorThief = new ColorThief(); 34 | var img = document.createElement("img"); 35 | img.src = base64; 36 | img.className = "hiddenImage"; 37 | 38 | document.querySelector(".emptyContainer").classList.add("hidden"); 39 | document.querySelector(".imagePreview").classList.remove("hidden"); 40 | document.querySelector(".imagePreview").style.backgroundImage = "url(" + base64 + ")"; 41 | document.querySelector(".colorPalette").classList.remove("hidden"); 42 | 43 | img.addEventListener("load", function(e) { 44 | var palette = colorThief.getPalette(img, 6); 45 | palette.forEach(function(color, i) { 46 | var li = document.querySelector(".colorPalette__color:nth-of-type(" + (i + 1) + ")"); 47 | li.style.backgroundColor = toHex(color); 48 | li.setAttribute("data-color", toHex(color)); 49 | 50 | var secondaryColor = toSecondaryColor(color); 51 | li.querySelector("svg path").setAttribute("fill", toHex(secondaryColor)); 52 | }); 53 | document.body.removeChild(img); 54 | }); 55 | 56 | document.body.appendChild(img); 57 | } 58 | 59 | document.addEventListener("DOMContentLoaded", function() { 60 | var colors = document.querySelectorAll(".colorPalette__color"); 61 | var toastTimeout; 62 | var currentColor; 63 | 64 | for (var i = 0; i < colors.length; i++) { 65 | colors[i].addEventListener("click", function() { 66 | document.querySelector(".imagePreview__toast a").classList.remove("collapsed"); 67 | var color = this.getAttribute("data-color"); 68 | currentColor = color; 69 | var data = { 70 | "type": "clipboard", 71 | "color": color, 72 | "date": new Date().getTime() 73 | } 74 | window.location.hash = JSON.stringify(data); 75 | document.querySelector(".imagePreview__toast p").innerHTML = "Copied to your clipboard."; 76 | document.querySelector(".imagePreview__toast span").style.backgroundColor = color; 77 | document.querySelector(".imagePreview__toast").classList.remove("hidden"); 78 | if (toastTimeout) clearTimeout(toastTimeout); 79 | toastTimeout = setTimeout(function () { 80 | document.querySelector(".imagePreview__toast").classList.add("hidden"); 81 | }, 3000); 82 | }); 83 | } 84 | 85 | document.querySelector(".imagePreview__toast a").addEventListener("click", function() { 86 | document.querySelector(".imagePreview__toast p").innerHTML = "Added to Document Colors"; 87 | document.querySelector(".imagePreview__toast a").classList.add("collapsed"); 88 | var data = { 89 | type: "document", 90 | color: currentColor, 91 | date: new Date().getTime() 92 | }; 93 | window.location.hash = JSON.stringify(data); 94 | if (toastTimeout) clearTimeout(toastTimeout); 95 | toastTimeout = setTimeout(function() { 96 | document.querySelector(".imagePreview__toast").classList.add("hidden"); 97 | }, 3000); 98 | }); 99 | }); 100 | 101 | document.addEventListener("contextmenu", function(e) { 102 | e.preventDefault(); 103 | }); 104 | -------------------------------------------------------------------------------- /Alembic.sketchplugin/Contents/Resources/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkward/Alembic/552640b2c9eb648da02ad1f490d1971c660b2720/Alembic.sketchplugin/Contents/Resources/icon@2x.png -------------------------------------------------------------------------------- /Alembic.sketchplugin/Contents/Resources/runner@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkward/Alembic/552640b2c9eb648da02ad1f490d1971c660b2720/Alembic.sketchplugin/Contents/Resources/runner@2x.png -------------------------------------------------------------------------------- /Alembic.sketchplugin/Contents/Sketch/MochaJSDelegate.js: -------------------------------------------------------------------------------- 1 | // 2 | // MochaJSDelegate.js 3 | // MochaJSDelegate 4 | // 5 | // Created by Matt Curtis 6 | // Copyright (c) 2015. All rights reserved. 7 | // 8 | 9 | var MochaJSDelegate = function(selectorHandlerDict){ 10 | var uniqueClassName = "MochaJSDelegate_DynamicClass_" + NSUUID.UUID().UUIDString(); 11 | 12 | var delegateClassDesc = MOClassDescription.allocateDescriptionForClassWithName_superclass_(uniqueClassName, NSObject); 13 | 14 | delegateClassDesc.registerClass(); 15 | 16 | // Handler storage 17 | 18 | var handlers = {}; 19 | 20 | // Define interface 21 | 22 | this.setHandlerForSelector = function(selectorString, func){ 23 | var handlerHasBeenSet = (selectorString in handlers); 24 | var selector = NSSelectorFromString(selectorString); 25 | 26 | handlers[selectorString] = func; 27 | 28 | if(!handlerHasBeenSet){ 29 | /* 30 | For some reason, Mocha acts weird about arguments: 31 | https://github.com/logancollins/Mocha/issues/28 32 | 33 | We have to basically create a dynamic handler with a likewise dynamic number of predefined arguments. 34 | */ 35 | 36 | var dynamicHandler = function(){ 37 | var functionToCall = handlers[selectorString]; 38 | 39 | if(!functionToCall) return; 40 | 41 | return functionToCall.apply(delegateClassDesc, arguments); 42 | }; 43 | 44 | var args = [], regex = /:/g; 45 | while(match = regex.exec(selectorString)) args.push("arg"+args.length); 46 | 47 | dynamicFunction = eval("(function("+args.join(",")+"){ return dynamicHandler.apply(this, arguments); })"); 48 | 49 | delegateClassDesc.addInstanceMethodWithSelector_function_(selector, dynamicFunction); 50 | } 51 | }; 52 | 53 | this.removeHandlerForSelector = function(selectorString){ 54 | delete handlers[selectorString]; 55 | }; 56 | 57 | this.getHandlerForSelector = function(selectorString){ 58 | return handlers[selectorString]; 59 | }; 60 | 61 | this.getAllHandlers = function(){ 62 | return handlers; 63 | }; 64 | 65 | this.getClass = function(){ 66 | return NSClassFromString(uniqueClassName); 67 | }; 68 | 69 | this.getClassInstance = function(){ 70 | return NSClassFromString(uniqueClassName).new(); 71 | }; 72 | 73 | // Conveience 74 | 75 | if(typeof selectorHandlerDict == "object"){ 76 | for(var selectorString in selectorHandlerDict){ 77 | this.setHandlerForSelector(selectorString, selectorHandlerDict[selectorString]); 78 | } 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /Alembic.sketchplugin/Contents/Sketch/alembic.js: -------------------------------------------------------------------------------- 1 | @import "MochaJSDelegate.js"; 2 | 3 | function changePanelHeight(panel, height) { 4 | var frame = panel.frame(); 5 | var previousHeight = frame.size.height; 6 | if (height == previousHeight) return; 7 | 8 | frame.origin.y += previousHeight - height; 9 | frame.size.height = height; 10 | panel.setFrame_display_animate(frame, true, true); 11 | } 12 | 13 | function expandPanel(panel) { 14 | changePanelHeight(panel, 196); 15 | } 16 | 17 | function minimizePanel(panel) { 18 | changePanelHeight(panel, 148); 19 | } 20 | 21 | function extractBase64FromSelection(selection) { 22 | if (selection.count() < 1 || selection.count() > 1) return undefined; 23 | 24 | var currentLayer = selection[0]; 25 | if (currentLayer.class() == "MSBitmapLayer") { 26 | var data = currentLayer.image().data(); 27 | return "data:image/png;base64," + data.base64EncodedStringWithOptions(null); 28 | } else { 29 | var fills = currentLayer.style().fills().reverse(); 30 | var fill = fills.find(function(e) { 31 | return e.image() 32 | }); 33 | 34 | if (fill) { 35 | var data = fill.image().data(); 36 | return "data:image/png;base64," + data.base64EncodedStringWithOptions(null); 37 | } else return undefined; 38 | } 39 | } 40 | 41 | function onRun(context) { 42 | var threadDictionary = NSThread.mainThread().threadDictionary(); 43 | var identifier = "co.awkward.alembic"; 44 | if (threadDictionary[identifier]) return; 45 | 46 | var webView = WebView.alloc().initWithFrame(NSMakeRect(0, 0, 294, 126)); 47 | var windowObject = webView.windowScriptObject(); 48 | 49 | var selection = context.selection; 50 | var base64 = extractBase64FromSelection(selection); 51 | 52 | COScript.currentCOScript().setShouldKeepAround_(true); 53 | 54 | var panel = NSPanel.alloc().init(); 55 | panel.setFrame_display(NSMakeRect(0, 0, 294, 170), true); 56 | panel.setStyleMask(NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSClosableWindowMask | NSFullSizeContentViewWindowMask); 57 | panel.setBackgroundColor(NSColor.whiteColor()); 58 | panel.setLevel(NSFloatingWindowLevel); 59 | panel.title = "Alembic"; 60 | panel.titlebarAppearsTransparent = true; 61 | panel.makeKeyAndOrderFront(null); 62 | panel.standardWindowButton(NSWindowMiniaturizeButton).setHidden(true); 63 | panel.standardWindowButton(NSWindowZoomButton).setHidden(true); 64 | panel.center() 65 | threadDictionary[identifier] = panel; 66 | 67 | var vibrancy = NSVisualEffectView.alloc().initWithFrame(NSMakeRect(0, 0, 294, 170)); 68 | vibrancy.setAppearance(NSAppearance.appearanceNamed(NSAppearanceNameVibrantLight)); 69 | vibrancy.setBlendingMode(NSVisualEffectBlendingModeBehindWindow); 70 | vibrancy.autoresizingMask = NSViewHeightSizable; 71 | 72 | var delegate = new MochaJSDelegate({ 73 | "webView:didFinishLoadForFrame:": (function(webView, webFrame) { 74 | if (base64 == undefined) { 75 | windowObject.evaluateWebScript("emptyState()"); 76 | } else { 77 | expandPanel(panel); 78 | windowObject.evaluateWebScript("update('" + base64 + "')"); 79 | } 80 | }), 81 | "webView:didChangeLocationWithinPageForFrame:": (function(webView, webFrame) { 82 | var hash = windowObject.evaluateWebScript("window.location.hash.substring(1)"); 83 | var data = JSON.parse(hash); 84 | 85 | if (data.type == "clipboard") { 86 | var clipboard = NSPasteboard.generalPasteboard(); 87 | clipboard.clearContents(); 88 | clipboard.setString_forType_(data.color, NSStringPboardType); 89 | } else if (data.type == "document") { 90 | var documentAssets = context.document.documentData().assets(); 91 | documentAssets.addColor(MSImmutableColor.colorWithSVGString(data.color).newMutableCounterpart()); 92 | NSApp.delegate().refreshCurrentDocument(); 93 | } 94 | }) 95 | }) 96 | 97 | webView.setFrameLoadDelegate_(delegate.getClassInstance()); 98 | 99 | webView.setDrawsBackground(false); 100 | var request = NSURLRequest.requestWithURL(context.plugin.urlForResourceNamed("alembic.html")); 101 | webView.autoresizingMask = NSViewHeightSizable; 102 | webView.mainFrame().loadRequest(request); 103 | 104 | panel.contentView().addSubview(vibrancy); 105 | panel.contentView().addSubview(webView); 106 | 107 | var closeButton = panel.standardWindowButton(NSWindowCloseButton); 108 | closeButton.setCOSJSTargetFunction(function(sender) { 109 | panel.close(); 110 | threadDictionary.removeObjectForKey(identifier); 111 | COScript.currentCOScript().setShouldKeepAround_(false); 112 | }); 113 | } 114 | 115 | var onSelectionChanged = function(context) { 116 | // BUG: newSelection is empty when changing selection 117 | // WORKAROUND: document.selectedLayers().layers() 118 | // http://sketchplugins.com/d/112-bug-selectionchanged-finish-newselection-is-empty 119 | 120 | var threadDictionary = NSThread.mainThread().threadDictionary(); 121 | var identifier = "co.awkward.alembic"; 122 | 123 | if (threadDictionary[identifier]) { 124 | var selection = context.actionContext.document.selectedLayers().layers(); 125 | var base64 = extractBase64FromSelection(selection); 126 | var panel = threadDictionary[identifier]; 127 | 128 | var webView = panel.contentView().subviews()[1]; 129 | var windowObject = webView.windowScriptObject(); 130 | 131 | expandPanel(panel); 132 | windowObject.evaluateWebScript(base64 == undefined ? null : "update('" + base64 + "')"); 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /Alembic.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Alembic", 3 | "identifier": "co.awkward.alembic", 4 | "version": "1.1.1", 5 | "description": "Extract colors from your images.", 6 | "appcast": 7 | "https://raw.githubusercontent.com/awkward/Alembic/master/.appcast.xml", 8 | "icon": "icon@2x.png", 9 | "author": "Awkward", 10 | "commands": [ 11 | { 12 | "script": "alembic.js", 13 | "handlers": { 14 | "run": "onRun", 15 | "actions": { 16 | "SelectionChanged.finish": "onSelectionChanged" 17 | } 18 | }, 19 | "name": "Alembic", 20 | "identifier": "co.awkward.alembic.command", 21 | "description": "Extract colors from your images.", 22 | "icon": "runner@2x.png" 23 | } 24 | ], 25 | "menu": { 26 | "isRoot": true, 27 | "items": ["co.awkward.alembic.command"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Docs/alembic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkward/Alembic/552640b2c9eb648da02ad1f490d1971c660b2720/Docs/alembic.gif -------------------------------------------------------------------------------- /Docs/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkward/Alembic/552640b2c9eb648da02ad1f490d1971c660b2720/Docs/download.png -------------------------------------------------------------------------------- /Docs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkward/Alembic/552640b2c9eb648da02ad1f490d1971c660b2720/Docs/icon.png -------------------------------------------------------------------------------- /Docs/runnerBadge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkward/Alembic/552640b2c9eb648da02ad1f490d1971c660b2720/Docs/runnerBadge.png -------------------------------------------------------------------------------- /Docs/runnerInstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkward/Alembic/552640b2c9eb648da02ad1f490d1971c660b2720/Docs/runnerInstall.png -------------------------------------------------------------------------------- /Docs/runnerUsage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkward/Alembic/552640b2c9eb648da02ad1f490d1971c660b2720/Docs/runnerUsage.gif -------------------------------------------------------------------------------- /Docs/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkward/Alembic/552640b2c9eb648da02ad1f490d1971c660b2720/Docs/usage.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 awkward 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | icon
3 | Alembic
4 |

5 | 6 | Contact 7 | 8 | 9 | License: MIT 10 | 11 |

12 |

13 | 14 |

15 | Alembic 16 |

17 | 18 |

19 | 20 | Download Alembic 21 | 22 | 23 | Sketch Runner 24 | 25 |

26 | 27 |
28 |
29 | 30 | A Sketch plugin to extract a color palette from any layer that contains bitmap data. This works for both images and layers with a Pattern fill. Alembic uses [Color Thief](https://github.com/lokesh/color-thief/) to extract a representative color palette. 31 | 32 | ## Introduction 33 | 34 | Hi, we’re [Awkward](https://awkward.co). A while ago, we came across this [Tweet](https://twitter.com/jeffreydgroot/status/889803683927601153) with an idea for a Sketch plugin that extracts color palettes from images. We felt like it was perfect to experiment with and the idea resulted in Alembic. 35 | 36 | ## Usage 37 | 38 | 1. Launch `Plugins › ⚗️ Alembic` 39 | 2. Select the layer you want to extract the colors from 40 | 3. Click a color to copy it to your clipboard 41 | 42 | Usage 43 | 44 | You can also use Sketch Runner to quickly launch Alembic. 45 | 46 | Sketch Runner 47 | 48 | ## Installation 49 | 50 | ### Using Sketch Runner 51 | 52 | With Sketch Runner, just go to the `install` command and search for `Alembic`. 53 | 54 | Install Alembic with Sketch Runner 55 | 56 | ### Using Sketchpacks 57 | 58 | Search for `Alembic` in [Sketchpacks](https://sketchpacks.com) or just [install it directly](https://sketchpacks.com/awkward/Alembic/install). 59 | 60 | ### Manually 61 | 62 | 1. Download the [latest release](https://github.com/awkward/Alembic/releases/download/v1.1.1/alembic.sketchplugin.zip) 63 | 2. Open `Alembic.sketchplugin` 64 | 65 | ## License 66 | 67 | > Alembic is available under the MIT license. See the LICENSE file for more info. 68 | --------------------------------------------------------------------------------