├── .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 |
11 |
12 |
⚗️ Could not extract palette
13 |
Select one layer that contains an image.
14 |
15 |
16 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
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 |
3 | Alembic
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
43 |
44 | You can also use Sketch Runner to quickly launch Alembic.
45 |
46 |
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 |
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 |
--------------------------------------------------------------------------------