').appendTo(fieldContainer).get(0));
119 | }
120 |
121 | getValue() {
122 | return this.value_ || tinycolor(this.params_.defaultValue || '#000');
123 | }
124 |
125 | setValue(val, pauseUi) {
126 | let oldValue = this.value_;
127 | this.value_ = (val.hasOwnProperty('_r'))
128 | ? val
129 | : tinycolor(val || this.params_.defaultValue || '#000');
130 | if (!pauseUi) {
131 | this.pickerWidget.setState({ color: this.value_.toRgb() });
132 | }
133 | this.notifyChanged_(val, oldValue);
134 | }
135 |
136 | serializeValue() {
137 | return this.getValue().toRgbString();
138 | }
139 |
140 | deserializeValue(s) {
141 | this.setValue(s);
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/app/components/components.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | .icon-button {
18 | position: relative;
19 | color: $colorBlackSecondary;
20 | display: flex;
21 | text-decoration: none;
22 | cursor: pointer;
23 | outline: 0;
24 |
25 | &:hover,
26 | &:focus {
27 | color: $colorBlackPrimary;
28 | }
29 |
30 | &::after {
31 | opacity: 0;
32 | content: '';
33 | display: block;
34 | position: absolute;
35 | left: 0;
36 | top: 0;
37 | right: 0;
38 | bottom: 0;
39 | border-radius: 50%;
40 | background-color: rgba($colorBlackPrimary, $opacityBlackHighlight);
41 | transition: opacity .2s ease;
42 | pointer-events: none;
43 | }
44 |
45 | &:active::after {
46 | opacity: 1;
47 | }
48 |
49 | &.theme-dark {
50 | color: $colorWhiteSecondary;
51 |
52 | &:hover,
53 | &:focus {
54 | color: $colorWhitePrimary;
55 | }
56 |
57 | &::after {
58 | background-color: rgba($colorWhitePrimary, $opacityWhiteHighlight);
59 | }
60 | }
61 | }
62 |
63 | .fab-button {
64 | border: 0;
65 | background-color: $colorAccent;
66 | box-shadow: material-shadow(4);
67 | border-radius: 50%;
68 | cursor: pointer;
69 | min-width: 0;
70 | min-height: 0;
71 | padding: 16px;
72 | position: relative;
73 | color: $colorWhitePrimary;
74 | display: flex;
75 | text-decoration: none;
76 | outline: 0;
77 | transition: box-shadow .2s ease;
78 |
79 | &::after {
80 | opacity: 0;
81 | content: '';
82 | display: block;
83 | position: absolute;
84 | left: 0;
85 | top: 0;
86 | right: 0;
87 | bottom: 0;
88 | border-radius: 50%;
89 | background-color: rgba($colorWhitePrimary, $opacityWhiteHighlight);
90 | transition: opacity .2s ease;
91 | pointer-events: none;
92 | }
93 |
94 | &:hover,
95 | &:focus {
96 | box-shadow: material-shadow(6);
97 |
98 | &::after {
99 | opacity: .3;
100 | }
101 | }
102 |
103 | &:active {
104 | box-shadow: material-shadow(8);
105 |
106 | &::after {
107 | opacity: .1;
108 | }
109 | }
110 |
111 | &[disabled] {
112 | background-color: rgba(#000, .12);
113 | box-shadow: none;
114 | color: $colorBlackTertiary;
115 | cursor: not-allowed;
116 | }
117 | }
118 |
119 | .tooltip {
120 | position: absolute;
121 | left: 50%;
122 | bottom: -8px;
123 | text-align: center;
124 | transform: translate(-50%, 100%);
125 | pointer-events: none;
126 | background-color: rgba(material-color('grey', '800'), .8);
127 | border-radius: 2px;
128 | color: $colorWhitePrimary;
129 | font-size: 12px;
130 | line-height: 16px;
131 | font-weight: 500;
132 | padding: 4px 8px;
133 | visibility: hidden;
134 | opacity: 0;
135 | transition:
136 | opacity .1s ease,
137 | visibility 0s ease .1s;
138 |
139 | :hover > &,
140 | :focus > & {
141 | transition: opacity .1s ease .2s;
142 | opacity: 1;
143 | visibility: visible;
144 | }
145 | }
146 |
147 | .checkbox {
148 | display: flex;
149 | flex-direction: row;
150 | align-items: center;
151 | cursor: pointer;
152 |
153 | input[type="checkbox"] {
154 | appearance: none;
155 | margin: 0 4px 0 0;
156 | display: flex;
157 | outline: 0;
158 |
159 | &::after {
160 | @include material-icons;
161 | content: 'check_box_outline_blank';
162 | color: $colorBlackSecondary;
163 | cursor: pointer;
164 | }
165 |
166 | &:checked::after {
167 | content: 'check_box';
168 | color: $colorPrimary;
169 | }
170 | }
171 | }
172 |
173 | .tabs {
174 | padding: 0;
175 | display: inline-flex;
176 | flex-direction: row;
177 | overflow: hidden;
178 | z-index: 1;
179 |
180 | input[type=radio] {
181 | display: none;
182 | }
183 |
184 | label {
185 | color: $colorBlackSecondary;
186 | font-size: 14px;
187 | line-height: 20px;
188 | font-weight: 500;
189 | text-transform: uppercase;
190 | letter-spacing: .5px;
191 | padding: 12px 16px;
192 | cursor: pointer;
193 | outline: 0;
194 |
195 | &:focus,
196 | &:active {
197 | background-color: rgba(#000, $opacityBlackHighlight);
198 | }
199 | }
200 |
201 | input:checked + label {
202 | color: $colorBlackPrimary;
203 | box-shadow: 0 -2px 0 $colorPrimary inset;
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/app/pages/ninepatch/nine-patch-preview.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import $ from 'jquery';
18 |
19 | export class NinePatchPreview {
20 | constructor(stage) {
21 | this.stage = stage;
22 | this.size = {w: 200, h: 200};
23 | this.setupUi();
24 | this.redraw();
25 | }
26 |
27 | setupUi() {
28 | let startWidth, startHeight, startX, startY;
29 |
30 | let mouseMoveHandler_ = ev => {
31 | this.size.w = Math.max(1, startWidth + (ev.pageX - startX) * 2);
32 | this.size.h = Math.max(1, startHeight + (ev.pageY - startY) * 2);
33 | this.redraw();
34 | };
35 |
36 | let mouseUpHandler_ = ev => {
37 | $(window)
38 | .off('mousemove', mouseMoveHandler_)
39 | .off('mouseup', mouseUpHandler_);
40 | };
41 |
42 | $('.preview-area')
43 | .on('mousedown', ev => {
44 | startWidth = this.size.w;
45 | startHeight = this.size.h;
46 | startX = ev.pageX;
47 | startY = ev.pageY;
48 |
49 | $(window)
50 | .on('mousemove', mouseMoveHandler_)
51 | .on('mouseup', mouseUpHandler_);
52 | });
53 |
54 | $('#preview-with-content').click(ev => $('.text-preview').toggle($(ev.currentTarget).is(':checked')));
55 | }
56 |
57 | redraw() {
58 | let canvas = $('.preview-area canvas').get(0);
59 | canvas.width = this.size.w;
60 | canvas.height = this.size.h;
61 |
62 | if (this.stage.srcCtx) {
63 | let ctx = canvas.getContext('2d');
64 |
65 | let fixed = {
66 | l: this.stage.stretchRect.x,
67 | t: this.stage.stretchRect.y,
68 | r: this.stage.srcSize.w - this.stage.stretchRect.x - this.stage.stretchRect.w,
69 | b: this.stage.srcSize.h - this.stage.stretchRect.y - this.stage.stretchRect.h
70 | };
71 |
72 | // TL
73 | if (fixed.l && fixed.t)
74 | ctx.drawImage(this.stage.srcCtx.canvas,
75 | 0, 0, fixed.l, fixed.t,
76 | 0, 0, fixed.l, fixed.t);
77 |
78 | // BL
79 | if (fixed.l && fixed.b)
80 | ctx.drawImage(this.stage.srcCtx.canvas,
81 | 0, this.stage.srcSize.h - fixed.b, fixed.l, fixed.b,
82 | 0, this.size.h - fixed.b, fixed.l, fixed.b);
83 |
84 | // TR
85 | if (fixed.r && fixed.t)
86 | ctx.drawImage(this.stage.srcCtx.canvas,
87 | this.stage.srcSize.w - fixed.r, 0, fixed.r, fixed.t,
88 | this.size.w - fixed.r, 0, fixed.r, fixed.t);
89 |
90 | // BR
91 | if (fixed.r && fixed.b)
92 | ctx.drawImage(this.stage.srcCtx.canvas,
93 | this.stage.srcSize.w - fixed.r, this.stage.srcSize.h - fixed.b, fixed.r, fixed.b,
94 | this.size.w - fixed.r, this.size.h - fixed.b, fixed.r, fixed.b);
95 |
96 | // Top
97 | if (fixed.t)
98 | ctx.drawImage(this.stage.srcCtx.canvas,
99 | fixed.l, 0, this.stage.stretchRect.w, fixed.t,
100 | fixed.l, 0, this.size.w - fixed.l - fixed.r, fixed.t);
101 |
102 | // Left
103 | if (fixed.l)
104 | ctx.drawImage(this.stage.srcCtx.canvas,
105 | 0, fixed.t, fixed.l, this.stage.stretchRect.h,
106 | 0, fixed.t, fixed.l, this.size.h - fixed.t - fixed.b);
107 |
108 | // Right
109 | if (fixed.r)
110 | ctx.drawImage(this.stage.srcCtx.canvas,
111 | this.stage.srcSize.w - fixed.r, fixed.t, fixed.r, this.stage.stretchRect.h,
112 | this.size.w - fixed.r, fixed.t, fixed.r, this.size.h - fixed.t - fixed.b);
113 |
114 | // Bottom
115 | if (fixed.b)
116 | ctx.drawImage(this.stage.srcCtx.canvas,
117 | fixed.l, this.stage.srcSize.h - fixed.b, this.stage.stretchRect.w, fixed.b,
118 | fixed.l, this.size.h - fixed.b, this.size.w - fixed.l - fixed.r, fixed.b);
119 |
120 | // Middle
121 | ctx.drawImage(this.stage.srcCtx.canvas,
122 | fixed.l, fixed.t, this.stage.stretchRect.w, this.stage.stretchRect.h,
123 | fixed.l, fixed.t, this.size.w - fixed.l - fixed.r, this.size.h - fixed.t - fixed.b);
124 |
125 | // preview content
126 | $('.preview-area .text-preview')
127 | .css({
128 | left: this.stage.contentRect.x + "px",
129 | top: this.stage.contentRect.y + "px",
130 | width: (this.size.w - this.stage.srcSize.w + this.stage.contentRect.w) + "px",
131 | height: (this.size.h - this.stage.srcSize.h + this.stage.contentRect.h) + "px"
132 | });
133 | }
134 | }
135 | }
--------------------------------------------------------------------------------
/app/pages/action-bar-icon-generator.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import $ from 'jquery';
18 | import tinycolor from 'tinycolor2';
19 |
20 | import * as studio from '../studio';
21 |
22 | import {BaseGenerator} from '../base-generator';
23 |
24 | const ICON_SIZE = { w: 24, h: 24 };
25 | const TARGET_RECT = { x: 0, y: 0, w: 24, h: 24 };
26 |
27 | const GRID_OVERLAY_SVG =
28 | `
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | `;
38 |
39 | export class ActionBarIconGenerator extends BaseGenerator {
40 | get gridOverlaySvg() {
41 | return GRID_OVERLAY_SVG;
42 | }
43 |
44 | setupForm() {
45 | super.setupForm();
46 |
47 | let defaultNameForSourceValue_ = v => {
48 | let name = studio.Util.sanitizeResourceName(v.name || 'example');
49 | return `ic_action_${name}`;
50 | };
51 |
52 | let nameField, customColorField;
53 | this.form = new studio.Form({
54 | id: 'iconform',
55 | container: '#inputs-form',
56 | fields: [
57 | new studio.ImageField('source', {
58 | title: 'Source',
59 | helpText: 'Must be transparent',
60 | maxFinalSize: { w: 128, h: 128 },
61 | clipartNoTrimPadding: true,
62 | defaultValueClipart: 'add_circle',
63 | dropTarget: document.body,
64 | onChange: (newValue, oldValue) => {
65 | if (nameField.getValue() == defaultNameForSourceValue_(oldValue)) {
66 | nameField.setValue(defaultNameForSourceValue_(newValue));
67 | }
68 | }
69 | }),
70 | (nameField = new studio.TextField('name', {
71 | newGroup: true,
72 | title: 'Name',
73 | helpText: 'Used when generating ZIP files.',
74 | defaultValue: defaultNameForSourceValue_({})
75 | })),
76 | new studio.EnumField('theme', {
77 | title: 'Theme',
78 | buttons: true,
79 | options: [
80 | { id: 'light', title: 'Light' },
81 | { id: 'dark', title: 'Dark' },
82 | { id: 'custom', title: 'Custom' }
83 | ],
84 | defaultValue: 'light'
85 | }),
86 | (customColorField = new studio.ColorField('color', {
87 | title: 'Color',
88 | defaultValue: 'rgba(33, 150, 243, .6)',
89 | alpha: true
90 | }))
91 | ]
92 | });
93 | this.form.onChange(field => {
94 | let values = this.form.getValues();
95 | $('.outputs-panel').attr('data-theme', values.theme);
96 | customColorField.setEnabled(values.theme == 'custom');
97 | this.regenerateDebounced_();
98 | });
99 | }
100 |
101 | regenerate() {
102 | let values = this.form.getValues();
103 | values.name = values.name || 'ic_action';
104 |
105 | this.zipper.clear();
106 | this.zipper.setZipFilename(`${values.name}.zip`);
107 |
108 | this.densities.forEach(density => {
109 | let mult = studio.Util.getMultBaseMdpi(density);
110 | let iconSize = studio.Util.multRound(ICON_SIZE, mult);
111 |
112 | let outCtx = studio.Drawing.context(iconSize);
113 | let tmpCtx = studio.Drawing.context(iconSize);
114 |
115 | if (values.source.ctx) {
116 | let srcCtx = values.source.ctx;
117 | studio.Drawing.drawCenterInside(
118 | tmpCtx,
119 | srcCtx,
120 | studio.Util.mult(TARGET_RECT, mult),
121 | {x: 0, y: 0, w: srcCtx.canvas.width, h: srcCtx.canvas.height});
122 | }
123 |
124 | let color = values.color;
125 | if (values.theme == 'light') {
126 | color = tinycolor('rgba(0, 0, 0, .54)');
127 | } else if (values.theme == 'dark') {
128 | color = tinycolor('#fff');
129 | }
130 |
131 | let alpha = color.getAlpha();
132 | color.setAlpha(1);
133 |
134 | studio.Effects.fx([
135 | {effect: 'fill-color', color: color.toRgbString(), opacity: alpha}
136 | ], outCtx, tmpCtx, iconSize);
137 |
138 | color.setAlpha(alpha);
139 |
140 | this.zipper.add({
141 | name: `res/drawable-${density}/${values.name}.png`,
142 | canvas: outCtx.canvas
143 | });
144 |
145 | this.setImageForSlot_(density, outCtx.canvas.toDataURL());
146 | });
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/app/lib/material-shadows.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // shadow values taken directly from angular material (as of v1.0.0-rc1)
18 |
19 | $shadow-key-umbra-opacity: .2;
20 | $shadow-key-penumbra-opacity: .14;
21 | $shadow-ambient-shadow-opacity: .12;
22 |
23 | $material-box-shadows: (
24 | 1: (0 1px 3px 0 rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 1px 1px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 2px 1px -1px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
25 | 2: (0 1px 5px 0 rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 2px 2px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 3px 1px -2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
26 | 3: (0 1px 8px 0 rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 3px 4px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 3px 3px -2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
27 | 4: (0 2px 4px -1px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 4px 5px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 1px 10px 0 rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
28 | 5: (0 3px 5px -1px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 5px 8px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 1px 14px 0 rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
29 | 6: (0 3px 5px -1px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 6px 10px 0 rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 1px 18px 0 rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
30 | 7: (0 4px 5px -2px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 7px 10px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 2px 16px 1px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
31 | 8: (0 5px 5px -3px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 8px 10px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 3px 14px 2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
32 | 9: (0 5px 6px -3px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 9px 12px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 3px 16px 2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
33 | 10: (0 6px 6px -3px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 10px 14px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 4px 18px 3px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
34 | 11: (0 6px 7px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 11px 15px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 4px 20px 3px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
35 | 12: (0 7px 8px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 12px 17px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 5px 22px 4px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
36 | 13: (0 7px 8px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 13px 19px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 5px 24px 4px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
37 | 14: (0 7px 9px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 14px 21px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 5px 26px 4px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
38 | 15: (0 8px 9px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 15px 22px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 6px 28px 5px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
39 | 16: (0 8px 10px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 16px 24px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 6px 30px 5px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
40 | 17: (0 8px 11px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 17px 26px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 6px 32px 5px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
41 | 18: (0 9px 11px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 18px 28px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 7px 34px 6px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
42 | 19: (0 9px 12px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 19px 29px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 7px 36px 6px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
43 | 20: (0 10px 13px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 20px 31px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 8px 38px 7px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
44 | 21: (0 10px 13px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 21px 33px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 8px 40px 7px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
45 | 22: (0 10px 14px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 22px 35px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 8px 42px 7px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
46 | 23: (0 11px 14px -7px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 23px 36px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 9px 44px 8px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
47 | 24: (0 11px 15px -7px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0 24px 38px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0 9px 46px 8px rgba(0, 0, 0, $shadow-ambient-shadow-opacity)),
48 | );
49 |
50 | @function material-shadow($z) {
51 | $shadow: map-get($material-box-shadows, $z);
52 | @if $shadow {
53 | @return $shadow;
54 | } @else {
55 | // Libsass still doesn't seem to support @error
56 | @warn '=> ERROR: NO SHADOW SPECIFIED AT GIVEN DEPTH.';
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/pages/ninepatch/nine-patch-generator.scss:
--------------------------------------------------------------------------------
1 | body.page-nine-patch-generator {
2 | // inputs
3 | .inputs-panel {
4 | width: 350px;
5 | }
6 |
7 | // stage
8 |
9 | .nine-patch-editor-area {
10 | flex: 1;
11 | display: flex;
12 | flex-direction: column;
13 | min-width: 0;
14 | }
15 |
16 | .stage-top-toolbar,
17 | .stage-bottom-toolbar {
18 | background-color: #fff;
19 | padding: 12px 20px;
20 | display: flex;
21 | flex-direction: row;
22 | align-items: center;
23 | z-index: 1;
24 | flex: 0 0 auto;
25 |
26 | .field-label {
27 | font-size: 13px;
28 | line-height: 16px;
29 | margin-right: 12px;
30 | color: $colorBlackSecondary;
31 | }
32 |
33 | button {
34 | margin-right: 8px;
35 | }
36 |
37 | .flex {
38 | flex: 1 0 auto;
39 | }
40 | }
41 |
42 | .stage-top-toolbar {
43 | box-shadow: 0 1px 0 $thinBorderColor;
44 | }
45 |
46 | .stage-bottom-toolbar {
47 | box-shadow: 0 -1px 0 $thinBorderColor;
48 | }
49 |
50 | .stage-which,
51 | .stage-grid-color {
52 | display: inline-flex;
53 | }
54 |
55 | .stage-bottom-controls {
56 | position: relative;
57 | }
58 |
59 | .stage-bottom-right-controls {
60 | position: absolute;
61 | top: 0;
62 | right: 0;
63 | }
64 |
65 | .nine-patch-stage {
66 | flex: 1;
67 | display: flex;
68 | flex-direction: column;
69 | // align-items: center;
70 | // justify-content: center;
71 | overflow: auto;
72 | padding: 24px;
73 | }
74 |
75 | .stage-canvas-container {
76 | position: relative;
77 | box-shadow: material-shadow(3);
78 | display: flex;
79 | flex: 0 0 auto;
80 | margin: auto;
81 |
82 | .empty {
83 | color: $colorBlackTertiary;
84 | max-width: 300px;
85 | padding: 32px 48px;
86 | }
87 |
88 | canvas {
89 | image-rendering: optimizeSpeed;
90 | image-rendering: -moz-crisp-edges;
91 | image-rendering: -webkit-optimize-contrast;
92 | image-rendering: optimize-contrast;
93 | image-rendering: pixelated;
94 | -ms-interpolation-mode: nearest-neighbor;
95 | }
96 |
97 | .overlay {
98 | position: absolute;
99 | left: 0;
100 | top: 0;
101 | right: 0;
102 | bottom: 0;
103 | }
104 | }
105 |
106 | .canvas-label {
107 | $canvasLabelColor: material-color('red', 'a400');
108 | $canvasLabelThickness: 1px;
109 |
110 | position: absolute;
111 | font-weight: bold;
112 | font-family: Roboto;
113 | color: $canvasLabelColor;
114 | pointer-events: none;
115 | font-size: 12px;
116 | line-height: 12px;
117 |
118 | text-align: center;
119 | display: flex;
120 | align-items: center;
121 | justify-content: center;
122 |
123 | &::before,
124 | &::after {
125 | flex: 1;
126 | background-color: rgba($canvasLabelColor, .2);
127 | content: '';
128 | }
129 |
130 | &.label-horizontal {
131 | transform: translateY(-50%);
132 | height: 10px;
133 | flex-direction: row;
134 |
135 | &::before,
136 | &::after {
137 | height: $canvasLabelThickness;
138 | }
139 |
140 | &::before {
141 | margin-right: 4px;
142 | }
143 |
144 | &::after {
145 | margin-left: 4px;
146 | }
147 | }
148 |
149 | &.label-vertical {
150 | transform: translateX(-50%);
151 | width: 10px;
152 | flex-direction: column;
153 |
154 | &::before,
155 | &::after {
156 | width: $canvasLabelThickness;
157 | }
158 |
159 | &::before {
160 | margin-bottom: 4px;
161 | }
162 |
163 | &::after {
164 | margin-top: 4px;
165 | }
166 | }
167 | }
168 |
169 | // outputs and preview area
170 |
171 | #download-zip-button {
172 | top: 24px;
173 | }
174 |
175 | .outputs-preview-sidebar {
176 | position: relative;
177 | display: flex;
178 | flex-direction: column;
179 | width: 400px;
180 | box-shadow: material-shadow(2);
181 | z-index: 1;
182 | overflow: hidden;
183 | }
184 |
185 | .outputs-preview-tabs {
186 | background-color: $colorPrimary700;
187 | flex: 0 0 auto;
188 |
189 | label {
190 | color: $colorWhiteSecondary;
191 | padding: 16px 20px;
192 |
193 | &:focus,
194 | &:active {
195 | background-color: rgba(#000, $opacityBlackHighlight);
196 | }
197 | }
198 |
199 | input:checked + label {
200 | color: $colorWhitePrimary;
201 | box-shadow: 0 -2px 0 $colorWhitePrimary inset;
202 | }
203 | }
204 |
205 | .outputs-panel,
206 | .nine-patch-preview-area-container {
207 | flex: 1 1 auto;
208 | display: none;
209 | background-color: material-color('grey', '300');
210 | }
211 |
212 | .outputs-panel {
213 | min-height: 0;
214 | }
215 |
216 | &[data-theme="dark"] {
217 | .outputs-panel,
218 | .nine-patch-preview-area-container {
219 | background-color: material-color('grey', '400');
220 | }
221 | }
222 |
223 | .outputs-preview-sidebar[data-view="preview"] {
224 | .nine-patch-preview-area-container {
225 | display: flex;
226 | }
227 | }
228 |
229 | .outputs-preview-sidebar[data-view="output"] {
230 | .outputs-panel {
231 | display: flex;
232 | }
233 | }
234 |
235 | .preview-area {
236 | flex: 1;
237 | position: relative;
238 | overflow: hidden;
239 | cursor: se-resize;
240 | user-select: none;
241 | }
242 |
243 | .preview-area .text-preview {
244 | position: absolute;
245 | overflow: hidden;
246 | width: 0;
247 | height: 0;
248 | display: none;
249 | }
250 |
251 | .preview-area canvas {
252 | pointer-events: none;
253 | }
254 |
255 | .preview-stage > div {
256 | position: relative;
257 | }
258 |
259 | #preview-with-content-container {
260 | position: absolute;
261 | top: 24px;
262 | left: 24px;
263 | }
264 |
265 | .preview-area.dark label {
266 | color: #eee;
267 | }
268 | .preview-stage {
269 | height: 100%;
270 | display: flex;
271 | align-items: center;
272 | justify-content: center;
273 | }
274 | }
--------------------------------------------------------------------------------
/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | const gulp = require('gulp');
18 | const $ = require('gulp-load-plugins')();
19 | const del = require('del');
20 | const browserSync = require('browser-sync');
21 | const reload = browserSync.reload;
22 | const merge = require('merge-stream');
23 | const path = require('path');
24 | const workboxBuild = require('workbox-build');
25 | const prettyBytes = require('pretty-bytes');
26 | const webpack = require('webpack');
27 | const sass = require('gulp-sass')(require('sass'));
28 |
29 | const AUTOPREFIXER_BROWSERS = [
30 | 'ff >= 30',
31 | 'chrome >= 34',
32 | 'safari >= 7',
33 | ];
34 |
35 |
36 | let DEV_MODE = false;
37 | let BASE_HREF = DEV_MODE ? '/' : '/AndroidAssetStudio/';
38 |
39 | let webpackInstance;
40 |
41 | function printWebpackStats(stats) {
42 | console.log(stats.toString({
43 | modules: false,
44 | colors: true,
45 | }));
46 | };
47 |
48 |
49 | function errorHandler(error) {
50 | if (error.fileName) {
51 | console.error(`Error in ${error.fileName}`);
52 | }
53 | console.error(error.stack);
54 | this.emit('end'); // http://stackoverflow.com/questions/23971388
55 | }
56 |
57 | // Lint JavaScript
58 | gulp.task('webpack', cb => {
59 | // force reload webpack config
60 | delete require.cache[require.resolve('./webpack.config.js')];
61 | let webpackConfig = require('./webpack.config.js');
62 | webpackConfig.mode = DEV_MODE ? 'development' : 'production';
63 | webpackInstance = webpack(webpackConfig, (err, stats) => {
64 | printWebpackStats(stats);
65 | cb();
66 | });
67 | });
68 |
69 | // Optimize Images
70 | gulp.task('res', () => {
71 | return gulp.src('app/res/**/*')
72 | .pipe($.cache($.imagemin({
73 | progressive: true,
74 | interlaced: true
75 | })))
76 | .pipe(gulp.dest('dist/res'));
77 | });
78 |
79 | // Copy All Files At The Root Level (app) and lib
80 | gulp.task('copy', () => {
81 | return merge(
82 | gulp.src([
83 | 'app/favicon.ico',
84 | 'app/sw.js',
85 | ], {dot: true, nodir: true,})
86 | .pipe(gulp.dest('dist')),
87 | gulp.src('older-version/**/*', {dot: true})
88 | .pipe(gulp.dest('dist/older-version')));
89 | });
90 |
91 | // Compile and Automatically Prefix Stylesheets
92 | gulp.task('styles', () => {
93 | // For best performance, don't add Sass partials to `gulp.src`
94 | return gulp.src('app/app.entry.scss')
95 | .pipe($.changed('styles', {extension: '.scss'}))
96 | .pipe($.sassGlob())
97 | .pipe(sass({
98 | style: 'expanded',
99 | precision: 10,
100 | quiet: true
101 | }).on('error', errorHandler))
102 | .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS))
103 | // Concatenate And Minify Styles
104 | .pipe($.if(!DEV_MODE, $.csso()))
105 | .pipe($.tap(file => file.path = file.path.replace(/\.entry\.css$/, '.css')))
106 | .pipe(gulp.dest('dist'));
107 | });
108 |
109 |
110 | gulp.task('html', () => {
111 | return gulp.src([
112 | 'app/**/*.html',
113 | '!app/**/_*.html'
114 | ])
115 | .pipe($.nucleus({
116 | templateRootPath: [
117 | 'app',
118 | ],
119 | }).on('error', errorHandler))
120 | .pipe($.replace(/%%BASE_HREF%%/g, BASE_HREF))
121 | .pipe($.if(!DEV_MODE, $.minifyHtml()))
122 | .pipe($.tap((file, t) => {
123 | if (file.contextData.destination) {
124 | file.path = path.join('./app', file.contextData.destination);
125 | }
126 | }))
127 | .pipe(gulp.dest('dist'));
128 | });
129 |
130 | // Clean Output Directory
131 | gulp.task('clean', cb => {
132 | del.sync(['.tmp', 'dist']);
133 | $.cache.clearAll();
134 | cb();
135 | });
136 |
137 | const setDevMode = cb => { DEV_MODE = true; cb(); }
138 |
139 | // Watch Files For Changes & Reload
140 | gulp.task('serve', gulp.series(setDevMode, 'copy', 'styles', 'html', 'webpack', () => {
141 | browserSync({
142 | notify: false,
143 | // Run as an https by uncommenting 'https: true'
144 | // Note: this uses an unsigned certificate which on first access
145 | // will present a certificate warning in the browser.
146 | // https: true,
147 | server: {
148 | baseDir: ['.tmp', 'dist', 'app'],
149 | },
150 | port: 3000,
151 | });
152 |
153 | let r = cb => { reload(); cb(); };
154 | gulp.watch(['app/**/*.html'], gulp.series('html', r));
155 | gulp.watch(['app/**/*.{scss,css}'], gulp.series('styles', r));
156 | gulp.watch(['app/res/**/*'], gulp.series('res', r));
157 |
158 | if (webpackInstance) {
159 | webpackInstance.watch({}, (err, stats) => {
160 | printWebpackStats(stats);
161 | reload();
162 | });
163 | }
164 | }));
165 |
166 | gulp.task('service-worker', () => {
167 | return workboxBuild.injectManifest({
168 | swSrc: path.join('app', 'sw-prod.js'),
169 | swDest: path.join('dist', 'sw.js'),
170 | globDirectory: 'dist',
171 | globIgnores: [
172 | 'older-version/**/*',
173 | ],
174 | globPatterns: [
175 | '*.html',
176 | '**/*.svg',
177 | '**/*.js',
178 | '**/*.css',
179 | ],
180 | }).then(obj => {
181 | obj.warnings.forEach(warning => console.warn(warning));
182 | console.log(`A service worker was generated to precache ${obj.count} files ` +
183 | `totalling ${prettyBytes(obj.size)}`);
184 | });
185 | });
186 |
187 | // Build Production Files, the Default Task
188 | gulp.task('default', gulp.series('clean', 'styles', gulp.parallel('webpack', 'html', 'res', 'copy'), 'service-worker'));
189 |
190 | // Build and serve the output from the dist build
191 | gulp.task('serve:dist', gulp.series('default', () => {
192 | browserSync({
193 | notify: false,
194 | server: 'dist',
195 | port: 3001,
196 | });
197 | }));
198 |
199 | // Deploy to GitHub pages
200 | gulp.task('deploy', () => {
201 | return gulp.src('dist/**/*', {dot: true})
202 | .pipe($.ghPages());
203 | });
204 |
--------------------------------------------------------------------------------
/app/studio/imagelib/analysis.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Drawing} from './drawing';
18 |
19 | export const Analysis = {};
20 |
21 | Analysis.TRIM_RECT_WORKER_JS = `
22 | self.onmessage = function(event) {
23 | var l = event.data.size.w, t = event.data.size.h, r = 0, b = 0;
24 |
25 | var alpha;
26 | for (var y = 0; y < event.data.size.h; y++) {
27 | for (var x = 0; x < event.data.size.w; x++) {
28 | alpha = event.data.imageData.data[
29 | ((y * event.data.size.w + x) << 2) + 3];
30 | if (alpha >= event.data.minAlpha) {
31 | l = Math.min(x, l);
32 | t = Math.min(y, t);
33 | r = Math.max(x, r);
34 | b = Math.max(y, b);
35 | }
36 | }
37 | }
38 |
39 | if (l > r) {
40 | // no pixels, couldn't trim
41 | postMessage({ x: 0, y: 0, w: event.data.size.w, h: event.data.size.h });
42 | return;
43 | }
44 |
45 | postMessage({ x: l, y: t, w: r - l + 1, h: b - t + 1 });
46 | };`
47 |
48 | Analysis.MAX_TRIM_SRC_SIZE = 500;
49 |
50 | Analysis.getTrimRect = function(ctx, size, minAlpha) {
51 | if (!ctx.canvas) {
52 | // Likely an image
53 | let src = ctx;
54 | ctx = Drawing.context(size);
55 | ctx.drawImage(src, 0, 0);
56 | }
57 |
58 | let scale = 1;
59 | if (size.w > Analysis.MAX_TRIM_SRC_SIZE || size.h > Analysis.MAX_TRIM_SRC_SIZE) {
60 | scale = (size.w > Analysis.MAX_TRIM_SRC_SIZE)
61 | ? Analysis.MAX_TRIM_SRC_SIZE / size.w
62 | : Analysis.MAX_TRIM_SRC_SIZE / size.h;
63 | let scaledSize = { w: size.w * scale, h: size.h * scale };
64 | let tmpCtx = Drawing.context(scaledSize);
65 | tmpCtx.drawImage(ctx.canvas, 0, 0, size.w, size.h, 0, 0, scaledSize.w, scaledSize.h);
66 | ctx = tmpCtx;
67 | size = scaledSize;
68 | }
69 |
70 | let worker;
71 | let promise = new Promise((resolve, reject) => {
72 | if (minAlpha == 0) {
73 | resolve({ x: 0, y: 0, w: size.w, h: size.h });
74 | }
75 |
76 | minAlpha = minAlpha || 1;
77 |
78 | worker = runWorkerJs_(
79 | Analysis.TRIM_RECT_WORKER_JS,
80 | {
81 | imageData: ctx.getImageData(0, 0, size.w, size.h),
82 | size,
83 | minAlpha
84 | },
85 | resultingRect => {
86 | resultingRect.x /= scale;
87 | resultingRect.y /= scale;
88 | resultingRect.w /= scale;
89 | resultingRect.h /= scale;
90 | resolve(resultingRect)
91 | worker = null;
92 | });
93 | });
94 |
95 | Object.defineProperty(promise, 'worker', {
96 | get: () => worker
97 | });
98 |
99 | return promise;
100 | };
101 |
102 | Analysis.getCenterOfMass = function(ctx, size, minAlpha) {
103 | return new Promise((resolve, reject) => {
104 | if (!ctx.canvas) {
105 | // Likely an image
106 | var src = ctx;
107 | ctx = Drawing.context(size);
108 | ctx.drawImage(src, 0, 0);
109 | }
110 |
111 | if (minAlpha == 0) {
112 | resolve({ x: size.w / 2, y: size.h / 2 });
113 | }
114 |
115 | minAlpha = minAlpha || 1;
116 |
117 | var l = size.w, t = size.h, r = 0, b = 0;
118 | var imageData = ctx.getImageData(0, 0, size.w, size.h);
119 |
120 | var sumX = 0;
121 | var sumY = 0;
122 | var n = 0; // number of pixels > minAlpha
123 | var alpha;
124 | for (var y = 0; y < size.h; y++) {
125 | for (var x = 0; x < size.w; x++) {
126 | alpha = imageData.data[((y * size.w + x) << 2) + 3];
127 | if (alpha >= minAlpha) {
128 | sumX += x;
129 | sumY += y;
130 | ++n;
131 | }
132 | }
133 | }
134 |
135 | if (n <= 0) {
136 | // no pixels > minAlpha, just use center
137 | resolve({ x: size.w / 2, h: size.h / 2 });
138 | }
139 |
140 | resolve({ x: Math.round(sumX / n), y: Math.round(sumY / n) });
141 | });
142 | };
143 |
144 |
145 | /**
146 | * Helper method for running inline Web Workers, if the browser can support
147 | * them. If the browser doesn't support inline Web Workers, run the script
148 | * on the main thread, with this function body's scope, using eval. Browsers
149 | * must provide BlobBuilder, URL.createObjectURL, and Worker support to use
150 | * inline Web Workers. Most features such as importScripts() are not
151 | * currently supported, so this only works for basic workers.
152 | * @param {String} js The inline Web Worker Javascript code to run. This code
153 | * must use 'self' and not 'this' as the global context variable.
154 | * @param {Object} params The parameters object to pass to the worker.
155 | * Equivalent to calling Worker.postMessage(params);
156 | * @param {Function} callback The callback to run when the worker calls
157 | * postMessage. Equivalent to adding a 'message' event listener on a
158 | * Worker object and running callback(event.data);
159 | */
160 | function runWorkerJs_(js, params, callback) {
161 | var URL = window.URL || window.webkitURL || window.mozURL;
162 | var Worker = window.Worker;
163 |
164 | if (URL && Worker && hasBlobConstructor_()) {
165 | // The Blob constructor, Worker, and window.URL.createObjectURL are all available,
166 | // so we can use inline workers.
167 | var bb = new Blob([js], {type:'text/javascript'});
168 | var worker = new Worker(URL.createObjectURL(bb));
169 | worker.onmessage = function(event) {
170 | callback(event.data);
171 | };
172 | worker.postMessage(params);
173 | return worker;
174 |
175 | } else {
176 | // We can't use inline workers, so run the worker JS on the main thread.
177 | (function() {
178 | var __DUMMY_OBJECT__ = {};
179 | // Proxy to Worker.onmessage
180 | var postMessage = function(result) {
181 | callback(result);
182 | };
183 | // Bind the worker to this dummy object. The worker will run
184 | // in this scope.
185 | eval('var self=__DUMMY_OBJECT__;\n' + js);
186 | // Proxy to Worker.postMessage
187 | __DUMMY_OBJECT__.onmessage({
188 | data: params
189 | });
190 | })();
191 |
192 | // Return a dummy Worker.
193 | return {
194 | terminate: function(){}
195 | };
196 | }
197 | };
198 |
199 | // https://github.com/gildas-lormeau/zip.js/issues/17#issuecomment-8513258
200 | // thanks Eric!
201 | function hasBlobConstructor_() {
202 | try {
203 | return !!new Blob();
204 | } catch(e) {
205 | return false;
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/app/lib/material-colors.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | $material-colors: (
18 | 'red': (
19 | '50': #ffebee,
20 | '100': #ffcdd2,
21 | '200': #ef9a9a,
22 | '300': #e57373,
23 | '400': #ef5350,
24 | '500': #f44336,
25 | '600': #e53935,
26 | '700': #d32f2f,
27 | '800': #c62828,
28 | '900': #b71c1c,
29 | 'a100': #ff8a80,
30 | 'a200': #ff5252,
31 | 'a400': #ff1744,
32 | 'a700': #d50000
33 | ),
34 |
35 | 'pink': (
36 | '50': #fce4ec,
37 | '100': #f8bbd0,
38 | '200': #f48fb1,
39 | '300': #f06292,
40 | '400': #ec407a,
41 | '500': #e91e63,
42 | '600': #d81b60,
43 | '700': #c2185b,
44 | '800': #ad1457,
45 | '900': #880e4f,
46 | 'a100': #ff80ab,
47 | 'a200': #ff4081,
48 | 'a400': #f50057,
49 | 'a700': #c51162
50 | ),
51 |
52 | 'purple': (
53 | '50': #f3e5f5,
54 | '100': #e1bee7,
55 | '200': #ce93d8,
56 | '300': #ba68c8,
57 | '400': #ab47bc,
58 | '500': #9c27b0,
59 | '600': #8e24aa,
60 | '700': #7b1fa2,
61 | '800': #6a1b9a,
62 | '900': #4a148c,
63 | 'a100': #ea80fc,
64 | 'a200': #e040fb,
65 | 'a400': #d500f9,
66 | 'a700': #aa00ff
67 | ),
68 |
69 | 'deep-purple': (
70 | '50': #ede7f6,
71 | '100': #d1c4e9,
72 | '200': #b39ddb,
73 | '300': #9575cd,
74 | '400': #7e57c2,
75 | '500': #673ab7,
76 | '600': #5e35b1,
77 | '700': #512da8,
78 | '800': #4527a0,
79 | '900': #311b92,
80 | 'a100': #b388ff,
81 | 'a200': #7c4dff,
82 | 'a400': #651fff,
83 | 'a700': #6200ea
84 | ),
85 |
86 | 'indigo': (
87 | '50': #e8eaf6,
88 | '100': #c5cae9,
89 | '200': #9fa8da,
90 | '300': #7986cb,
91 | '400': #5c6bc0,
92 | '500': #3f51b5,
93 | '600': #3949ab,
94 | '700': #303f9f,
95 | '800': #283593,
96 | '900': #1a237e,
97 | 'a100': #8c9eff,
98 | 'a200': #536dfe,
99 | 'a400': #3d5afe,
100 | 'a700': #304ffe
101 | ),
102 |
103 | 'blue': (
104 | '50': #e3f2fd,
105 | '100': #bbdefb,
106 | '200': #90caf9,
107 | '300': #64b5f6,
108 | '400': #42a5f5,
109 | '500': #2196f3,
110 | '600': #1e88e5,
111 | '700': #1976d2,
112 | '800': #1565c0,
113 | '900': #0d47a1,
114 | 'a100': #82b1ff,
115 | 'a200': #448aff,
116 | 'a400': #2979ff,
117 | 'a700': #2962ff
118 | ),
119 |
120 | 'light-blue': (
121 | '50': #e1f5fe,
122 | '100': #b3e5fc,
123 | '200': #81d4fa,
124 | '300': #4fc3f7,
125 | '400': #29b6f6,
126 | '500': #03a9f4,
127 | '600': #039be5,
128 | '700': #0288d1,
129 | '800': #0277bd,
130 | '900': #01579b,
131 | 'a100': #80d8ff,
132 | 'a200': #40c4ff,
133 | 'a400': #00b0ff,
134 | 'a700': #0091ea
135 | ),
136 |
137 | 'cyan': (
138 | '50': #e0f7fa,
139 | '100': #b2ebf2,
140 | '200': #80deea,
141 | '300': #4dd0e1,
142 | '400': #26c6da,
143 | '500': #00bcd4,
144 | '600': #00acc1,
145 | '700': #0097a7,
146 | '800': #00838f,
147 | '900': #006064,
148 | 'a100': #84ffff,
149 | 'a200': #18ffff,
150 | 'a400': #00e5ff,
151 | 'a700': #00b8d4
152 | ),
153 |
154 | 'teal': (
155 | '50': #e0f2f1,
156 | '100': #b2dfdb,
157 | '200': #80cbc4,
158 | '300': #4db6ac,
159 | '400': #26a69a,
160 | '500': #009688,
161 | '600': #00897b,
162 | '700': #00796b,
163 | '800': #00695c,
164 | '900': #004d40,
165 | 'a100': #a7ffeb,
166 | 'a200': #64ffda,
167 | 'a400': #1de9b6,
168 | 'a700': #00bfa5
169 | ),
170 |
171 | 'green': (
172 | '50': #e8f5e9,
173 | '100': #c8e6c9,
174 | '200': #a5d6a7,
175 | '300': #81c784,
176 | '400': #66bb6a,
177 | '500': #4caf50,
178 | '600': #43a047,
179 | '700': #388e3c,
180 | '800': #2e7d32,
181 | '900': #1b5e20,
182 | 'a100': #b9f6ca,
183 | 'a200': #69f0ae,
184 | 'a400': #00e676,
185 | 'a700': #00c853
186 | ),
187 |
188 | 'light-green': (
189 | '50': #f1f8e9,
190 | '100': #dcedc8,
191 | '200': #c5e1a5,
192 | '300': #aed581,
193 | '400': #9ccc65,
194 | '500': #8bc34a,
195 | '600': #7cb342,
196 | '700': #689f38,
197 | '800': #558b2f,
198 | '900': #33691e,
199 | 'a100': #ccff90,
200 | 'a200': #b2ff59,
201 | 'a400': #76ff03,
202 | 'a700': #64dd17
203 | ),
204 |
205 | 'lime': (
206 | '50': #f9fbe7,
207 | '100': #f0f4c3,
208 | '200': #e6ee9c,
209 | '300': #dce775,
210 | '400': #d4e157,
211 | '500': #cddc39,
212 | '600': #c0ca33,
213 | '700': #afb42b,
214 | '800': #9e9d24,
215 | '900': #827717,
216 | 'a100': #f4ff81,
217 | 'a200': #eeff41,
218 | 'a400': #c6ff00,
219 | 'a700': #aeea00
220 | ),
221 |
222 | 'yellow': (
223 | '50': #fffde7,
224 | '100': #fff9c4,
225 | '200': #fff59d,
226 | '300': #fff176,
227 | '400': #ffee58,
228 | '500': #ffeb3b,
229 | '600': #fdd835,
230 | '700': #fbc02d,
231 | '800': #f9a825,
232 | '900': #f57f17,
233 | 'a100': #ffff8d,
234 | 'a200': #ffff00,
235 | 'a400': #ffea00,
236 | 'a700': #ffd600
237 | ),
238 |
239 | 'amber': (
240 | '50': #fff8e1,
241 | '100': #ffecb3,
242 | '200': #ffe082,
243 | '300': #ffd54f,
244 | '400': #ffca28,
245 | '500': #ffc107,
246 | '600': #ffb300,
247 | '700': #ffa000,
248 | '800': #ff8f00,
249 | '900': #ff6f00,
250 | 'a100': #ffe57f,
251 | 'a200': #ffd740,
252 | 'a400': #ffc400,
253 | 'a700': #ffab00
254 | ),
255 |
256 | 'orange': (
257 | '50': #fff3e0,
258 | '100': #ffe0b2,
259 | '200': #ffcc80,
260 | '300': #ffb74d,
261 | '400': #ffa726,
262 | '500': #ff9800,
263 | '600': #fb8c00,
264 | '700': #f57c00,
265 | '800': #ef6c00,
266 | '900': #e65100,
267 | 'a100': #ffd180,
268 | 'a200': #ffab40,
269 | 'a400': #ff9100,
270 | 'a700': #ff6d00
271 | ),
272 |
273 | 'deep-orange': (
274 | '50': #fbe9e7,
275 | '100': #ffccbc,
276 | '200': #ffab91,
277 | '300': #ff8a65,
278 | '400': #ff7043,
279 | '500': #ff5722,
280 | '600': #f4511e,
281 | '700': #e64a19,
282 | '800': #d84315,
283 | '900': #bf360c,
284 | 'a100': #ff9e80,
285 | 'a200': #ff6e40,
286 | 'a400': #ff3d00,
287 | 'a700': #dd2c00
288 | ),
289 |
290 | 'brown': (
291 | '50': #efebe9,
292 | '100': #d7ccc8,
293 | '200': #bcaaa4,
294 | '300': #a1887f,
295 | '400': #8d6e63,
296 | '500': #795548,
297 | '600': #6d4c41,
298 | '700': #5d4037,
299 | '800': #4e342e,
300 | '900': #3e2723
301 | ),
302 |
303 | 'grey': (
304 | '50': #fafafa,
305 | '100': #f5f5f5,
306 | '200': #eeeeee,
307 | '300': #e0e0e0,
308 | '400': #bdbdbd,
309 | '500': #9e9e9e,
310 | '600': #757575,
311 | '700': #616161,
312 | '800': #424242,
313 | '900': #212121
314 | ),
315 |
316 | 'blue-grey': (
317 | '50': #eceff1,
318 | '100': #cfd8dc,
319 | '200': #b0bec5,
320 | '300': #90a4ae,
321 | '400': #78909c,
322 | '500': #607d8b,
323 | '600': #546e7a,
324 | '700': #455a64,
325 | '800': #37474f,
326 | '900': #263238,
327 | '1000': #11171a
328 | )
329 | );
330 |
331 | @function material-color($color-name, $color-variant: '500') {
332 | $color: map-get(map-get($material-colors, $color-name), $color-variant);
333 | @if $color {
334 | @return $color;
335 | } @else {
336 | // Libsass still doesn't seem to support @error
337 | @warn '=> ERROR: COLOR NOT FOUND! <= | Your $color-name, $color-variant combination did not match any of the values in the $material-colors map.';
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/app/pages/ninepatch/nine-patch-generator.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import $ from 'jquery';
18 |
19 | import * as studio from '../../studio';
20 |
21 | import {BaseGenerator} from '../../base-generator';
22 |
23 | import {NinePatchStage} from './nine-patch-stage';
24 | import {NinePatchPreview} from './nine-patch-preview';
25 | import {NinePatchLoader} from './nine-patch-loader';
26 |
27 |
28 | const DENSITIES = new Set(['xxxhdpi', 'xxhdpi', 'xhdpi', 'hdpi', 'mdpi']);
29 | const SOURCE_DENSITY_OPTIONS = [
30 | { id: '160', title: 'mdpi
(160) ' },
31 | { id: '240', title: 'hdpi
(240) ' },
32 | { id: '320', title: 'xhdpi
(320) ' },
33 | { id: '480', title: 'xxhdpi
(480) ' },
34 | { id: '640', title: 'xxxhdpi
(640) ' }
35 | ];
36 |
37 |
38 | if (document.location.search.indexOf('extradensities') >= 0) {
39 | DENSITIES.add('ldpi');
40 | DENSITIES.add('tvdpi');
41 | // SOURCE_DENSITY_OPTIONS.push({ id: '120', title: 'ldpi
(120) ' });
42 | // SOURCE_DENSITY_OPTIONS.push({ id: '213', title: 'tvdpi
(213) ' });
43 | }
44 |
45 |
46 | export class NinePatchGenerator extends BaseGenerator {
47 | constructor() {
48 | super();
49 | this.stage = new NinePatchStage();
50 | this.preview = new NinePatchPreview(this.stage);
51 |
52 | this.stage.onChange(() => {
53 | this.regenerate();
54 | this.preview.redraw();
55 | });
56 |
57 | this.setupOutputsPreviewTabs();
58 | }
59 |
60 | setupOutputsPreviewTabs() {
61 | $('.outputs-preview-tabs input').on('change', ev => {
62 | $('.outputs-preview-sidebar').attr('data-view', $(ev.currentTarget).val());
63 | $('.outputs-preview-tabs input').prop('checked', false);
64 | $(ev.currentTarget).prop('checked', true);
65 | });
66 | }
67 |
68 | get densities() {
69 | return DENSITIES;
70 | }
71 |
72 | setupForm() {
73 | super.setupForm();
74 | let nameField;
75 | this.form = new studio.Form({
76 | id: 'ninepatchform',
77 | container: '#inputs-form',
78 | fields: [
79 | new studio.ImageField('source', {
80 | title: 'Source graphic',
81 | imageOnly: true,
82 | noTrimForm: true,
83 | noPreview: true,
84 | dropTarget: document.body
85 | }),
86 | new studio.EnumField('sourceDensity', {
87 | title: 'Source density',
88 | buttons: true,
89 | options: SOURCE_DENSITY_OPTIONS,
90 | defaultValue: '320'
91 | }),
92 | (nameField = new studio.TextField('name', {
93 | title: 'Drawable name',
94 | helpText: 'Used when generating ZIP files. Becomes
<name>.9.png.',
95 | defaultValue: 'example'
96 | }))
97 | ]
98 | });
99 | this.form.onChange(field => {
100 | let values = this.form.getValues();
101 | if (!field || field.id_ == 'source') {
102 | if (values.source) {
103 | if (!values.source.ctx) {
104 | return;
105 | }
106 | let src = values.source;
107 | let size = { w: src.ctx.canvas.width, h: src.ctx.canvas.height };
108 | this.stage.name = `${src.name}-${size.w}x${size.h}`;
109 | // let isSvg = !!src.name.match(/\.svg$/i);
110 | if (src.name && src.name.match(/\.9\.png$/i)) {
111 | NinePatchLoader.loadNinePatchIntoStage(src.ctx, this.stage);
112 | } else {
113 | this.stage.loadSourceImage(src.ctx);
114 | }
115 | if (src.name) {
116 | let name = studio.Util.sanitizeResourceName(src.name);
117 | if (name != nameField.getValue()) {
118 | nameField.setValue(name);
119 | }
120 | }
121 | } else {
122 | this.stage.loadSourceImage(null);
123 | }
124 | } else {
125 | this.regenerate();
126 | }
127 | });
128 | }
129 |
130 | regenerate() {
131 | // this.preview.update();
132 |
133 | if (!this.stage.srcCtx) {
134 | return;
135 | }
136 |
137 | let values = this.form.getValues();
138 | values.name = values.name || 'outline';
139 |
140 | this.zipper.clear();
141 | this.zipper.setZipFilename(`${values.name}.9.zip`);
142 |
143 | this.densities.forEach(density => {
144 | let dpi = studio.Util.getDpiForDensity(density);
145 |
146 | // scale source graphic
147 | // TODO: support better-smoothing option
148 | let scale = dpi / values.sourceDensity;
149 | let outSize = {
150 | w: Math.ceil(this.stage.srcSize.w * scale) + 2,
151 | h: Math.ceil(this.stage.srcSize.h * scale) + 2
152 | };
153 | let outCtx = studio.Drawing.context(outSize);
154 | studio.Drawing.drawImageScaled(outCtx, this.stage.srcCtx,
155 | 0, 0, this.stage.srcSize.w, this.stage.srcSize.h,
156 | 1, 1, outSize.w - 2, outSize.h - 2);
157 |
158 | // draw optical bounds
159 | fillRectImageData(outCtx, [255,0,0,255],
160 | 1, outSize.h - 1,
161 | Math.floor(scale * this.stage.opticalBoundsRect.x), 1);
162 | fillRectImageData(outCtx, [255,0,0,255],
163 | outSize.w - 1, outSize.h - 1,
164 | -Math.ceil(scale * (this.stage.srcSize.w - this.stage.opticalBoundsRect.x - this.stage.opticalBoundsRect.w)), 1);
165 | fillRectImageData(outCtx, [255,0,0,255],
166 | outSize.w - 1, 1,
167 | 1, Math.floor(scale * this.stage.opticalBoundsRect.y));
168 | fillRectImageData(outCtx, [255,0,0,255],
169 | outSize.w - 1, outSize.h - 1,
170 | 1, -Math.ceil(scale * (this.stage.srcSize.h - this.stage.opticalBoundsRect.y - this.stage.opticalBoundsRect.h)));
171 |
172 | // draw nine-patch tick marks
173 | fillRectImageData(outCtx, [0,0,0,255],
174 | 1 + Math.floor(scale * this.stage.stretchRect.x), 0,
175 | Math.ceil(scale * this.stage.stretchRect.w), 1);
176 | fillRectImageData(outCtx, [0,0,0,255],
177 | 0, 1 + Math.floor(scale * this.stage.stretchRect.y),
178 | 1, Math.ceil(scale * this.stage.stretchRect.h));
179 | fillRectImageData(outCtx, [0,0,0,255],
180 | 1 + Math.floor(scale * this.stage.contentRect.x), outSize.h - 1,
181 | Math.ceil(scale * this.stage.contentRect.w), 1);
182 | fillRectImageData(outCtx, [0,0,0,255],
183 | outSize.w - 1, 1 + Math.floor(scale * this.stage.contentRect.y),
184 | 1, Math.ceil(scale * this.stage.contentRect.h));
185 |
186 | // add to zip and show preview
187 |
188 | console.log(density, outCtx.getImageData(outSize.w - 1, Math.floor(outSize.h / 2), 1, 1).data.toString());
189 |
190 | this.zipper.add({
191 | name: `res/drawable-${density}/${values.name}.9.png`,
192 | canvas: outCtx.canvas
193 | });
194 |
195 | this.setImageForSlot_(density, outCtx.canvas.toDataURL('image/png', 1.0));
196 | });
197 | }
198 | }
199 |
200 | function fillRectImageData(ctx, colorArray, x, y, w, h) {
201 | if (w == 0 || h == 0) {
202 | return;
203 | }
204 |
205 | if (w < 0) {
206 | x += w;
207 | w = -w;
208 | }
209 | if (h < 0) {
210 | y += h;
211 | h = -h;
212 | }
213 |
214 | // This is necessary because fillRect() and other drawing methods have weird
215 | // alpha channel precision issues
216 | // see https://stackoverflow.com/questions/22384423/canvas-corrupts-rgb-when-alpha-0
217 | // see https://github.com/romannurik/AndroidAssetStudio/issues/196
218 | let imgData = ctx.createImageData(w, h);
219 | for (let i = 0; i < w * h; i++) {
220 | imgData.data[i * 4] = colorArray[0];
221 | imgData.data[i * 4 + 1] = colorArray[1];
222 | imgData.data[i * 4 + 2] = colorArray[2];
223 | imgData.data[i * 4 + 3] = colorArray[3];
224 | }
225 | ctx.putImageData(imgData, x, y);
226 | }
--------------------------------------------------------------------------------
/app/studio/imagelib/effects.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import tinycolor from 'tinycolor2';
18 |
19 | import {Drawing} from './drawing';
20 |
21 | const OUTER_EFFECTS = new Set(['outer-shadow', 'cast-shadow']);
22 | const INNER_EFFECTS = new Set(['inner-shadow', 'score']);
23 | const FILL_EFFECTS = new Set(['fill-color', 'fill-lineargradient', 'fill-radialgradient']);
24 |
25 |
26 | export const Effects = {
27 | fx(effects, dstCtx, src, size) {
28 | effects = effects || [];
29 |
30 | let outerEffects = effects.filter(e => !!e && OUTER_EFFECTS.has(e.effect));
31 | let innerEffects = effects.filter(e => !!e && INNER_EFFECTS.has(e.effect));
32 | let fillEffects = effects.filter(e => !!e && FILL_EFFECTS.has(e.effect));
33 |
34 | let tmpCtx, bufferCtx;
35 |
36 | // First render outer effects
37 | let padLeft, padRight, padBottom, padTop;
38 | padLeft = padRight = padBottom = padTop =
39 | outerEffects.reduce((r, e) => Math.max(r, e.blur || 0), 0);
40 |
41 | let paddedSize = {
42 | w: size.w + padLeft + padRight,
43 | h: size.h + padTop + padBottom
44 | };
45 |
46 | tmpCtx = Drawing.context(paddedSize);
47 |
48 | outerEffects.forEach(effect => {
49 | switch (effect.effect) {
50 | case 'cast-shadow':
51 | tmpCtx.clearRect(0, 0, paddedSize.w, paddedSize.h);
52 | tmpCtx.drawImage(src.canvas || src, padLeft, padTop);
53 | renderCastShadow_(tmpCtx, paddedSize.w, paddedSize.h);
54 | dstCtx.drawImage(tmpCtx.canvas, padLeft, padTop, size.w, size.h, 0, 0, size.w, size.h);
55 | break;
56 |
57 | case 'outer-shadow':
58 | let tColor = tinycolor(effect.color || '#000');
59 | let alpha = tColor.getAlpha();
60 | tColor.setAlpha(1);
61 |
62 | if (supportsCanvasFilters_()) {
63 | tmpCtx.save();
64 | tmpCtx.clearRect(0, 0, paddedSize.w, paddedSize.h);
65 | tmpCtx.filter = `blur(${effect.blur || 0}px)`;
66 | tmpCtx.drawImage(src.canvas || src, padLeft, padTop);
67 | tmpCtx.globalCompositeOperation = 'source-atop';
68 | tmpCtx.fillStyle = tColor.toRgbString();
69 | tmpCtx.fillRect(0, 0, paddedSize.w, paddedSize.h);
70 | tmpCtx.restore();
71 |
72 | dstCtx.save();
73 | dstCtx.translate(effect.translateX || 0, effect.translateY || 0);
74 | dstCtx.globalAlpha = alpha;
75 | dstCtx.drawImage(tmpCtx.canvas, padLeft, padTop, size.w, size.h, 0, 0, size.w, size.h);
76 | dstCtx.restore();
77 | } else {
78 | dstCtx.save();
79 | dstCtx.globalAlpha = alpha;
80 | dstCtx.shadowOffsetX = paddedSize.w;
81 | dstCtx.shadowOffsetY = 0;
82 | dstCtx.shadowColor = tColor.toRgbString();
83 | dstCtx.shadowBlur = canvasShadowBlurForRadius_(effect.blur || 0);
84 | dstCtx.drawImage(src.canvas || src,
85 | (effect.translateX || 0) - paddedSize.w,
86 | (effect.translateY || 0));
87 | dstCtx.restore();
88 | }
89 | break;
90 | }
91 | });
92 |
93 | // Next, render the source, fill effects (first one), and inner effects
94 | // in a buffer (bufferCtx)
95 | bufferCtx = Drawing.context(size);
96 | tmpCtx = Drawing.context(size);
97 | tmpCtx.drawImage(src.canvas || src, 0, 0);
98 | tmpCtx.globalCompositeOperation = 'source-atop';
99 |
100 | // Fill effects
101 | let fillOpacity = 1.0;
102 | fillEffects.forEach(effect => {
103 | fillOpacity = ('opacity' in effect) ? effect.opacity : 1;
104 |
105 | tmpCtx.save();
106 |
107 | switch (effect.effect) {
108 | case 'fill-color': {
109 | tmpCtx.fillStyle = effect.color;
110 | break;
111 | }
112 |
113 | case 'fill-lineargradient': {
114 | let gradient = tmpCtx.createLinearGradient(
115 | effect.fromX, effect.fromY, effect.toX, effect.toY);
116 | effect.colors.forEach(({offset, color}) => gradient.addColorStop(offset, color));
117 | tmpCtx.fillStyle = gradient;
118 | break;
119 | }
120 |
121 | case 'fill-radialgradient': {
122 | let gradient = tmpCtx.createRadialGradient(
123 | effect.centerX, effect.centerY, 0, effect.centerX, effect.centerY, effect.radius);
124 | effect.colors.forEach(({offset, color}) => gradient.addColorStop(offset, color));
125 | tmpCtx.fillStyle = gradient;
126 | break;
127 | }
128 | }
129 |
130 | tmpCtx.fillRect(0, 0, size.w, size.h);
131 | tmpCtx.restore();
132 | });
133 |
134 | bufferCtx.save();
135 | bufferCtx.globalAlpha = fillOpacity;
136 | bufferCtx.drawImage(tmpCtx.canvas, 0, 0);
137 | bufferCtx.restore();
138 |
139 | // Render inner effects
140 | padLeft = padTop = padRight = padBottom = 0;
141 | innerEffects.forEach(effect => {
142 | padLeft = Math.max(padLeft, (effect.blur || 0) + Math.max(0, (effect.translateX || 0)));
143 | padTop = Math.max(padTop, (effect.blur || 0) + Math.max(0, (effect.translateY || 0)));
144 | padRight = Math.max(padRight, (effect.blur || 0) + Math.max(0, -(effect.translateX || 0)));
145 | padBottom = Math.max(padBottom, (effect.blur || 0) + Math.max(0, -(effect.translateY || 0)));
146 | });
147 |
148 | paddedSize = {
149 | w: size.w + padLeft + padRight,
150 | h: size.h + padTop + padBottom
151 | };
152 |
153 | tmpCtx = Drawing.context(paddedSize);
154 |
155 | innerEffects.forEach(effect => {
156 | switch (effect.effect) {
157 | case 'inner-shadow':
158 | tmpCtx.save();
159 | tmpCtx.clearRect(0, 0, paddedSize.w, paddedSize.h);
160 | if (supportsCanvasFilters_()) {
161 | tmpCtx.filter = `blur(${effect.blur || 0}px)`;
162 | tmpCtx.drawImage(bufferCtx.canvas,
163 | padLeft + (effect.translateX || 0),
164 | padTop + (effect.translateY || 0));
165 | } else {
166 | tmpCtx.shadowOffsetX = paddedSize.w;
167 | tmpCtx.shadowOffsetY = 0;
168 | tmpCtx.shadowColor = '#000'; // color doesn't matter
169 | tmpCtx.shadowBlur = canvasShadowBlurForRadius_(effect.blur || 0);
170 | tmpCtx.drawImage(bufferCtx.canvas,
171 | padLeft + (effect.translateX || 0) - paddedSize.w,
172 | padTop + (effect.translateY || 0));
173 | }
174 | tmpCtx.globalCompositeOperation = 'source-out';
175 | tmpCtx.fillStyle = effect.color;
176 | tmpCtx.fillRect(0, 0, paddedSize.w, paddedSize.h);
177 | tmpCtx.restore();
178 |
179 | bufferCtx.save();
180 | bufferCtx.globalCompositeOperation = 'source-atop';
181 | bufferCtx.drawImage(tmpCtx.canvas, -padLeft, -padTop);
182 | bufferCtx.restore();
183 | break;
184 | }
185 | });
186 |
187 | // Draw buffer (source, fill, inner effects) on top of outer effects
188 | dstCtx.drawImage(bufferCtx.canvas, 0, 0);
189 | }
190 | }
191 |
192 |
193 | function renderCastShadow_(ctx, w, h) {
194 | let tmpCtx = Drawing.context({w, h});
195 | // render the cast shadow
196 | for (let o = 1; o < Math.max(w, h); o++) {
197 | tmpCtx.drawImage(ctx.canvas, o, o);
198 | }
199 | tmpCtx.globalCompositeOperation = 'source-in';
200 | tmpCtx.fillStyle = '#000';
201 | tmpCtx.fillRect(0, 0, w, h);
202 | let gradient = tmpCtx.createLinearGradient(0, 0, w, h);
203 | gradient.addColorStop(0, 'rgba(0, 0, 0, .2)');
204 | gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
205 | tmpCtx.fillStyle = gradient;
206 | tmpCtx.fillRect(0, 0, w, h);
207 | ctx.clearRect(0, 0, w, h);
208 | ctx.drawImage(tmpCtx.canvas, 0, 0);
209 | }
210 |
211 |
212 | function supportsCanvasFilters_() {
213 | if (!supportsCanvasFilters_.hasOwnProperty('cached')) {
214 | supportsCanvasFilters_.cached = (
215 | document.createElement('canvas').getContext('2d').filter == 'none');
216 | }
217 |
218 | return supportsCanvasFilters_.cached;
219 | }
220 |
221 |
222 | // determined empirically: http://codepen.io/anon/pen/ggLOqJ
223 | const BLUR_MULTIPLIER = [
224 | {re: /chrome/i, mult: 2.7},
225 | {re: /safari/i, mult: 1.8},
226 | {re: /firefox/i, mult: 1.7},
227 | {re: /./i, mult: 1.7}, // default
228 | ].find(x => x.re.test(navigator.userAgent)).mult;
229 |
230 |
231 | function canvasShadowBlurForRadius_(radius) {
232 | return radius * BLUR_MULTIPLIER;
233 | }
234 |
--------------------------------------------------------------------------------
/app/components/inputs-panel.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | $formFieldPaddingX: 12px;
18 | $formFieldPaddingY: 4px;
19 | $formFieldSpacing: 24px;
20 |
21 | .inputs-panel {
22 | box-sizing: border-box;
23 | background-color: material-color('grey', '100');
24 | width: 400px;
25 | flex: 0 0 auto;
26 | overflow-x: hidden;
27 | overflow-y: auto;
28 | box-shadow: material-shadow(2);
29 | padding: 24px 0;
30 | z-index: 2;
31 |
32 | .form-field-outer {
33 | padding: 0 24px;
34 |
35 | .form-field-outer {
36 | // nested forms
37 | padding: 0;
38 | }
39 | }
40 | }
41 |
42 | .form-field-outer {
43 | margin-bottom: $formFieldSpacing;
44 |
45 | > label {
46 | display: block;
47 | font-size: 13px;
48 | line-height: 20px;
49 | font-weight: 500;
50 | margin: 0 0 8px 0;
51 | color: $colorBlackPrimary;
52 | }
53 |
54 | > label .form-field-help-text {
55 | font-weight: 400;
56 | font-size: 12px;
57 | line-height: 16px;
58 | color: $colorBlackTertiary;
59 | }
60 |
61 | &[disabled] {
62 | opacity: .2;
63 | pointer-events: none;
64 | user-select: none;
65 | filter: grayscale(100%);
66 | }
67 |
68 | &.is-new-group {
69 | border-top: 1px solid $thinBorderColor;
70 | padding-top: 24px;
71 | }
72 | }
73 |
74 | .form-field-container {
75 | display: flex;
76 | flex-direction: column;
77 | }
78 |
79 | @mixin form-field-element {
80 | align-self: flex-start;
81 | border-radius: 2px;
82 | border: 0;
83 | box-shadow: 0 0 0 1px $thinBorderColor;
84 | padding: $formFieldPaddingY $formFieldPaddingX;
85 | font-family: Roboto, sans-serif;
86 | font-size: 14px;
87 | line-height: 20px;
88 | background-color: #fff;
89 | outline: 0;
90 |
91 | &:focus {
92 | box-shadow: 0 0 0 2px $colorPrimary;
93 | }
94 |
95 | &[disabled] {
96 | opacity: .5;
97 | user-select: none;
98 | cursor: not-allowed;
99 | filter: grayscale(100%);
100 | }
101 | }
102 |
103 | .sp-replacer {
104 | // color field
105 | @include form-field-element;
106 | padding: $formFieldPaddingY; // Even padding for this element
107 | }
108 |
109 | .form-field-select {
110 | position: relative;
111 | display: inline-flex;
112 | align-self: flex-start;
113 |
114 | select {
115 | @include form-field-element;
116 | padding:
117 | $formFieldPaddingY ($formFieldPaddingX + 16px) $formFieldPaddingY $formFieldPaddingX;
118 | cursor: pointer;
119 | appearance: none;
120 | }
121 |
122 | &::after {
123 | @include material-icons;
124 | content: 'arrow_drop_down';
125 | position: absolute;
126 | color: $colorBlackSecondary;
127 | right: 4px;
128 | top: 50%;
129 | transform: translateY(-50%);
130 | pointer-events: none;
131 | }
132 | }
133 |
134 | .form-field-image > .form-field-container {
135 | align-items: stretch;
136 | }
137 |
138 | .form-field-text {
139 | @include form-field-element;
140 | align-self: stretch;
141 | }
142 |
143 | .form-field-buttonset {
144 | @include form-field-element;
145 | padding: 0;
146 | display: inline-flex;
147 | flex-direction: row;
148 | overflow: hidden;
149 |
150 | input[type=radio] {
151 | display: none;
152 | }
153 |
154 | label {
155 | font-size: 14px;
156 | line-height: 20px;
157 | padding: $formFieldPaddingY $formFieldPaddingX;
158 | cursor: pointer;
159 | outline: 0;
160 |
161 | &:focus,
162 | &:active {
163 | background-color: $colorPrimary100;
164 | }
165 | }
166 |
167 | input:checked + label {
168 | background-color: $colorPrimary;
169 | color: $colorWhitePrimary;
170 |
171 | &:focus,
172 | &:active {
173 | background-color: $colorPrimary600;
174 | }
175 | }
176 | }
177 |
178 | .form-field-button {
179 | @include form-field-element;
180 | border: 0;
181 | font-size: 14px;
182 | line-height: 20px;
183 | padding: $formFieldPaddingY $formFieldPaddingX;
184 | cursor: pointer;
185 | outline: 0;
186 |
187 | &:active {
188 | background-color: $colorPrimary100;
189 | }
190 | }
191 |
192 | .form-image-hidden-file-field {
193 | position: absolute;
194 | left: -10000px;
195 | opacity: 0;
196 | }
197 |
198 | .form-image-type-params-clipart {
199 | position: relative;
200 | display: flex;
201 | flex-direction: column;
202 | margin-top: 8px;
203 | box-shadow: material-shadow(2);
204 | border-radius: 2px;
205 | background-color: #fff;
206 | overflow: hidden;
207 |
208 | &.is-hidden {
209 | display: none;
210 | }
211 | }
212 |
213 | // put the ::before on the parent container, not the input
214 | // because inputs can't have pseudoelements
215 | .form-image-type-params-clipart::before {
216 | @include material-icons;
217 | content: 'search';
218 | display: block;
219 | color: $colorBlackTertiary;
220 | font-size: 20px;
221 | position: absolute;
222 | left: 12px;
223 | top: 8px;
224 | z-index: 2;
225 | }
226 |
227 | .form-image-clipart-filter {
228 | color: $colorBlackPrimary;
229 | font-size: 14px;
230 | line-height: 20px;
231 | border: 0;
232 | padding: 8px 8px 8px 40px;
233 | outline: 0;
234 | box-shadow: 0 1px 0 $thinBorderColor;
235 | z-index: 1;
236 |
237 | &::placeholder {
238 | color: $colorBlackTertiary;
239 | }
240 | }
241 |
242 | .form-image-clipart-list {
243 | height: 200px;
244 | padding: 16px;
245 | display: flex;
246 | flex-flow: row wrap;
247 | align-content: flex-start;
248 | box-sizing: border-box;
249 | overflow-y: scroll;
250 | }
251 |
252 | .form-image-clipart-item {
253 | @include material-icons;
254 | font-size: 36px;
255 | overflow: hidden;
256 | width: 36px;
257 | height: 36px;
258 | padding: 8px;
259 | cursor: pointer;
260 | border-radius: 50%;
261 |
262 | color: $colorBlackPrimary;
263 | transition:
264 | color .1s ease,
265 | background-color .1s ease,
266 | box-shadow .1s ease;
267 |
268 | &:hover,
269 | &:focus {
270 | background-color: rgba(#000, $opacityBlackHighlight);
271 | }
272 |
273 | &.is-selected {
274 | color: $colorWhitePrimary;
275 | box-shadow: 0 0 0 4px $colorPrimary;
276 | background-color: $colorPrimary;
277 | }
278 | }
279 |
280 | .form-image-clipart-attribution {
281 | font-size: 12px;
282 | line-height: 16px;
283 | padding: 4px 8px;
284 | color: $colorBlackTertiary;
285 | box-shadow: 0 -1px 0 $thinBorderColor;
286 | }
287 |
288 | .form-image-preview {
289 | display: none !important;
290 | background-color: #fff;
291 | // display: inline-block;
292 | max-height: 100px;
293 | max-width: 250px;
294 | border: 1px solid #ccc;
295 | }
296 |
297 | .form-field-range {
298 | display: flex;
299 | flex-direction: row;
300 | align-items: center;
301 |
302 | input[type="range"] {
303 | flex: 1;
304 | outline: 0;
305 | cursor: pointer;
306 | vertical-align: bottom;
307 | appearance: none;
308 | position: relative;
309 | height: $formFieldPaddingY * 2 + 20px;
310 | margin: 0;
311 | background-color: transparent;
312 |
313 | &::-webkit-slider-runnable-track {
314 | appearance: none;
315 | background-color: $colorPrimary;
316 | height: 2px;
317 | transform: translateY(calc(-50%));
318 | }
319 |
320 | &:focus::-webkit-slider-thumb {
321 | transform: translateY(calc(-50% + 1px)) scale(1.2);
322 | }
323 |
324 | &:active::-webkit-slider-thumb {
325 | transform: translateY(calc(-50% + 1px)) scale(1.5);
326 | }
327 |
328 | &::-webkit-slider-thumb {
329 | appearance: none;
330 | background: $colorPrimary;
331 | width: 16px;
332 | height: 16px;
333 | border-radius: 50%;
334 | transform: translateY(calc(-50% + 1px));
335 | transition: transform .1s ease;
336 | }
337 | }
338 | }
339 |
340 | .form-field-range-text {
341 | flex: 0 0 auto;
342 | margin-left: 6px;
343 | font-size: 14px;
344 | line-height: 20px;
345 | color: $colorPrimary;
346 | font-weight: 500;
347 | width: 40px;
348 | text-align: right;
349 | }
350 |
351 | .form-field-drop-target.drag-hover {
352 | position: relative;
353 |
354 | &::after {
355 | content: '';
356 | position: absolute;
357 | left: 0;
358 | top: 0;
359 | right: 0;
360 | bottom: 0;
361 | background-color: rgba($colorAccent, .5);
362 | animation: pulsate-color .33s ease 0s infinite alternate;
363 | z-index: 1000;
364 |
365 | @keyframes pulsate-color {
366 | from { opacity: .5 }
367 | to { opacity: 1 }
368 | }
369 | }
370 | }
371 |
372 | .form-subform {
373 | margin-top: $formFieldSpacing;
374 |
375 | &.is-hidden {
376 | display: none;
377 | }
378 |
379 | &:not(.is-hidden) + .form-subform {
380 | margin-top: 0;
381 | }
382 | }
383 |
384 | .form-field-color {
385 | &-popup-container {
386 | position: fixed;
387 | z-index: 2;
388 | }
389 |
390 | &-popup-cover {
391 | position: fixed;
392 | top: 0;
393 | right: 0;
394 | bottom: 0;
395 | left: 0;
396 | }
397 |
398 | &-widget {
399 | @include form-field-element;
400 | display: inline-flex;
401 | cursor: pointer;
402 | padding: 6px;
403 | }
404 |
405 | &-widget-swatch {
406 | width: 40px;
407 | height: 20px;
408 | box-shadow: 0 0 0 1px rgba(#000, .12);
409 | position: relative;
410 |
411 | &::before,
412 | &::after {
413 | content: '';
414 | position: absolute;
415 | left: 0;
416 | top: 0;
417 | right: 0;
418 | bottom: 0;
419 | }
420 |
421 | &::before {
422 | background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCA0MCAyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMCAwaDEwdjEwSDBWMHptMjAgMGgxMHYxMEgyMFYwek0xMCAxMGgxMHYxMEgxMFYxMHptMjAgMGgxMHYxMEgzMFYxMHoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbC1vcGFjaXR5PSIuMSIvPjwvc3ZnPg==");
423 | }
424 |
425 | &::after {
426 | background-color: currentColor;
427 | }
428 | }
429 | }
--------------------------------------------------------------------------------
/app/pages/launcher-icon-generator.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import * as studio from '../studio';
18 |
19 | import {BaseGenerator} from '../base-generator';
20 |
21 | const ICON_SIZE = { w: 48, h: 48 }; // now legacy
22 |
23 | const ADAPTIVE_ICON_WIDTH = 108;
24 |
25 | const TARGET_RECTS_BY_SHAPE = {
26 | circle: { x: 2, y: 2, w: 44, h: 44 },
27 | square: { x: 5, y: 5, w: 38, h: 38 },
28 | vrect: { x: 8, y: 2, w: 32, h: 44 },
29 | hrect: { x: 2, y: 8, w: 44, h: 32 },
30 | };
31 |
32 | const TARGET_RECT_FULL_BLEED = {x: 0, y: 0, w: 48, h: 48};
33 |
34 | const TARGET_RECT_ADAPTIVE = {x: 8, y: 8, w: 32, h: 32}; // same as middle 72dp in 108dp square
35 |
36 |
37 | const DEFAULT_EFFECT_OPTIONS = [
38 | { id: 'none', title: 'None' },
39 | { id: 'elevate', title: 'Elevate' },
40 | { id: 'shadow', title: 'Cast shadow' },
41 | { id: 'score', title: 'Score' }
42 | ];
43 |
44 | export class LauncherIconGenerator extends BaseGenerator {
45 | get densities() {
46 | return new Set(['xxxhdpi', 'xxhdpi', 'xhdpi', 'hdpi', 'mdpi']);
47 | }
48 |
49 | get outputSlots() {
50 | return new Set(['play_store', ...this.densities]);
51 | }
52 |
53 | setupForm() {
54 | this.form = new studio.Form({
55 | id: 'iconform',
56 | container: '#inputs-form',
57 | fields: [
58 | new studio.ImageField('foreground', {
59 | title: 'Foreground',
60 | maxFinalSize: { w: 720, h: 720 }, // max render size, for SVGs
61 | defaultValueTrim: 1,
62 | defaultValuePadding: .25,
63 | defaultValueClipart: 'android',
64 | dropTarget: document.body
65 | }),
66 | new studio.ColorField('foreColor', {
67 | newGroup: true,
68 | title: 'Color',
69 | helpText: 'Set to transparent to use original colors',
70 | alpha: true,
71 | defaultValue: 'rgba(96, 125, 139, 0)'
72 | }),
73 | new studio.ColorField('backColor', {
74 | title: 'Background color',
75 | defaultValue: '#448aff'
76 | }),
77 | new studio.BooleanField('crop', {
78 | title: 'Scaling',
79 | defaultValue: false,
80 | offText: 'Center',
81 | onText: 'Crop'
82 | }),
83 | new studio.EnumField('backgroundShape', {
84 | title: 'Shape (Legacy)',
85 | helpText: 'For older Android devices',
86 | options: [
87 | { id: 'square', title: 'Square' },
88 | { id: 'circle', title: 'Circle' },
89 | { id: 'vrect', title: 'Tall rect' },
90 | { id: 'hrect', title: 'Wide rect' }
91 | ],
92 | defaultValue: 'circle',
93 | }),
94 | new studio.EnumField('effects', {
95 | title: 'Effect',
96 | buttons: true,
97 | options: DEFAULT_EFFECT_OPTIONS,
98 | defaultValue: 'none'
99 | }),
100 | new studio.TextField('name', {
101 | title: 'Name',
102 | defaultValue: 'ic_launcher'
103 | })
104 | ]
105 | });
106 | this.form.onChange(field => this.regenerateDebounced_());
107 | }
108 |
109 | regenerate() {
110 | let values = this.form.getValues();
111 | values.name = values.name || 'ic_launcher';
112 |
113 | this.zipper.clear();
114 | this.zipper.setZipFilename(`${values.name}.zip`);
115 |
116 | // generate for each density
117 | for (let density of this.densities) {
118 | let mult = studio.Util.getMultBaseMdpi(density);
119 |
120 | // legacy version
121 | let ctx = this.regenerateRaw_({ mult });
122 | this.zipper.add({
123 | name: `res/mipmap-${density}/${values.name}.png`,
124 | canvas: ctx.canvas
125 | });
126 | this.setImageForSlot_(density, ctx.canvas.toDataURL());
127 |
128 | // adaptive version background + foreground
129 | this.zipper.add({
130 | name: `res/mipmap-${density}/${values.name}_adaptive_back.png`,
131 | canvas: this.regenerateRaw_({
132 | mult: mult * ADAPTIVE_ICON_WIDTH / ICON_SIZE.w,
133 | adaptive: 'back',
134 | }).canvas
135 | });
136 |
137 | this.zipper.add({
138 | name: `res/mipmap-${density}/${values.name}_adaptive_fore.png`,
139 | canvas: this.regenerateRaw_({
140 | mult: mult * ADAPTIVE_ICON_WIDTH / ICON_SIZE.w,
141 | adaptive: 'fore',
142 | }).canvas
143 | });
144 | }
145 |
146 | // generate web/play version
147 | let ctx = this.regenerateRaw_({ mult: 512 / 48, fullBleed: true });
148 | this.zipper.add({
149 | name: 'play_store_512.png',
150 | canvas: ctx.canvas
151 | });
152 | this.setImageForSlot_('play_store', ctx.canvas.toDataURL());
153 |
154 | this.zipper.add({
155 | name: '1024.png',
156 | canvas: this.regenerateRaw_({ mult: 1024 / 48, fullBleed: true }).canvas
157 | });
158 |
159 | // generate adaptive launcher XML
160 | this.zipper.add({
161 | name: `res/mipmap-anydpi-v26/${values.name}.xml`,
162 | textData: this.makeAdaptiveIconXml_(values.name)
163 | });
164 | }
165 |
166 | makeAdaptiveIconXml_(name) {
167 | return (
168 | `
169 |
170 |
171 |
172 | `);
173 | }
174 |
175 | regenerateRaw_({ mult, fullBleed, adaptive }) {
176 | let values = this.form.getValues();
177 | let foreSrcCtx = values.foreground ? values.foreground.ctx : null;
178 |
179 | let iconSize = studio.Util.multRound(ICON_SIZE, mult);
180 | let targetRect = TARGET_RECTS_BY_SHAPE[values.backgroundShape];
181 | if (fullBleed) {
182 | targetRect = TARGET_RECT_FULL_BLEED;
183 | } else if (adaptive) {
184 | targetRect = TARGET_RECT_ADAPTIVE;
185 | }
186 |
187 | let outCtx = studio.Drawing.context(iconSize);
188 |
189 | let backgroundLayer = {
190 | // background layer
191 | draw: ctx => {
192 | ctx.scale(mult, mult);
193 | values.backColor.setAlpha(1);
194 | ctx.fillStyle = values.backColor.toRgbString();
195 | if (fullBleed || adaptive) {
196 | ctx.fillRect(0, 0, ICON_SIZE.w, ICON_SIZE.h);
197 | return;
198 | }
199 |
200 | let targetRect = TARGET_RECTS_BY_SHAPE[values.backgroundShape];
201 | switch (values.backgroundShape) {
202 | case 'square':
203 | case 'vrect':
204 | case 'hrect':
205 | studio.Util.roundRectPath(ctx, targetRect, 3);
206 | ctx.fill();
207 | break;
208 |
209 | case 'circle':
210 | ctx.beginPath();
211 | ctx.arc(
212 | targetRect.x + targetRect.w / 2,
213 | targetRect.y + targetRect.h / 2,
214 | targetRect.w / 2,
215 | 0, 2 * Math.PI, false);
216 | ctx.closePath();
217 | ctx.fill();
218 | break;
219 | }
220 | },
221 | mask: true
222 | };
223 |
224 | let foregroundLayer = {
225 | // foreground content layer
226 | draw: ctx => {
227 | if (!foreSrcCtx) {
228 | return;
229 | }
230 |
231 | let drawFn_ = studio.Drawing[values.crop ? 'drawCenterCrop' : 'drawCenterInside'];
232 | drawFn_(ctx, foreSrcCtx, studio.Util.mult(targetRect, mult),
233 | {x: 0, y: 0, w: foreSrcCtx.canvas.width, h: foreSrcCtx.canvas.height});
234 | },
235 | effects: [],
236 | };
237 |
238 | if (values.effects == 'shadow') {
239 | foregroundLayer.effects.push({effect: 'cast-shadow'});
240 | }
241 |
242 | if (values.foreColor.getAlpha()) {
243 | foregroundLayer.effects.push({
244 | effect: 'fill-color',
245 | color: values.foreColor.toRgbString()
246 | });
247 | }
248 |
249 | if (values.effects == 'elevate' || values.effects == 'shadow') {
250 | foregroundLayer.effects = [
251 | ...foregroundLayer.effects,
252 | {
253 | effect: 'outer-shadow',
254 | color: 'rgba(0, 0, 0, 0.2)',
255 | translateY: .25 * mult
256 | },
257 | {
258 | effect: 'outer-shadow',
259 | color: 'rgba(0, 0, 0, 0.2)',
260 | blur: 1 * mult,
261 | translateY: 1 * mult
262 | }
263 | ];
264 | }
265 |
266 | let finalEffects = [
267 | {
268 | effect: 'inner-shadow',
269 | color: 'rgba(255, 255, 255, 0.2)',
270 | translateY: .25 * mult
271 | },
272 | {
273 | effect: 'inner-shadow',
274 | color: 'rgba(0, 0, 0, 0.2)',
275 | translateY: -.25 * mult
276 | },
277 | {
278 | effect: 'outer-shadow',
279 | color: 'rgba(0, 0, 0, 0.3)',
280 | blur: .7 * mult,
281 | translateY: .7 * mult
282 | },
283 | {
284 | effect: 'fill-radialgradient',
285 | centerX: 0,
286 | centerY: 0,
287 | radius: iconSize.w,
288 | colors: [
289 | { offset: 0, color: 'rgba(255,255,255,.1)' },
290 | { offset: 1.0, color: 'rgba(255,255,255,0)' }
291 | ]
292 | }
293 | ];
294 |
295 | if (fullBleed || adaptive) {
296 | finalEffects = finalEffects.filter(e => e.effect.match(/fill/));
297 | }
298 |
299 | studio.Drawing.drawLayers(outCtx, iconSize, {
300 | children: [
301 | (!adaptive || adaptive == 'back') && backgroundLayer,
302 | (!adaptive || adaptive == 'fore') && foregroundLayer,
303 | (values.effects == 'score' && adaptive !== 'back') && {
304 | draw: ctx => {
305 | ctx.fillStyle = 'rgba(0, 0, 0, .1)';
306 | ctx.fillRect(0, 0, iconSize.w, iconSize.h / 2);
307 | }
308 | },
309 | ],
310 | effects: finalEffects,
311 | });
312 |
313 | return outCtx;
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/app/pages/ninepatch/nine-patch-stage.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import $ from 'jquery';
18 |
19 | import {NinePatchTrimming} from './nine-patch-trimming';
20 |
21 | const EMPTY_RECT = {x: 0, y: 0, w: 0, h: 0};
22 |
23 | const SLOP_PIXELS = 10;
24 |
25 | export class NinePatchStage {
26 | constructor() {
27 | this.zoom = 1;
28 | this.matteColor = 'light';
29 | this.editMode = 'stretch';
30 | this.stretchRect = Object.assign({}, EMPTY_RECT);
31 | this.contentRect = Object.assign({}, EMPTY_RECT);
32 | this.opticalBoundsRect = Object.assign({}, EMPTY_RECT);
33 | this.name = 'default';
34 | this.changeListeners_ = [];
35 |
36 | this.$stage = $('.nine-patch-stage');
37 | this.$canvasContainer = $('.stage-canvas-container');
38 |
39 | this.setupUi();
40 | this.setupDragging();
41 |
42 | $(window).on('resize', () => {
43 | this.relayout();
44 | this.redrawOverlay();
45 | });
46 | }
47 |
48 | onChange(listener) {
49 | this.changeListeners_.push(listener);
50 | }
51 |
52 | notifyChange_() {
53 | this.changeListeners_.forEach(fn => fn());
54 | }
55 |
56 | setupUi() {
57 | // Stage code
58 | this.$topLabel = $('
').addClass('canvas-label label-vertical').hide().appendTo('body');
59 | this.$leftLabel = $('
').addClass('canvas-label label-horizontal').hide().appendTo('body');
60 | this.$rightLabel = $('
').addClass('canvas-label label-horizontal').hide().appendTo('body');
61 | this.$bottomLabel = $('
').addClass('canvas-label label-vertical').hide().appendTo('body');
62 |
63 | $('.stage-which input').on('change', ev => {
64 | this.editMode = $(ev.currentTarget).val();
65 | $('.trim-button').toggle(this.editMode == 'stretch');
66 | $('.find-region-button').text({
67 | stretch: 'Auto-stretch',
68 | padding: 'Auto-padding',
69 | opticalbounds: 'Auto-bounds'
70 | }[this.editMode]);
71 | $('.stage-which input').prop('checked', false);
72 | $(ev.currentTarget).prop('checked', true);
73 | this.redrawOverlay();
74 | });
75 |
76 | $('.stage-matte-color input').on('change', ev => {
77 | this.matteColor = $(ev.currentTarget).val();
78 | $(document.body).attr('data-theme', this.matteColor);
79 | $('.stage-matte-color input').prop('checked', false);
80 | $(ev.currentTarget).prop('checked', true);
81 | this.redrawImage();
82 | });
83 |
84 | $('.trim-edge-button').click(() => NinePatchTrimming.trimEdges(this));
85 | $('.trim-stretch-button').click(() => NinePatchTrimming.trimStretchRegion(this));
86 | $('.find-region-button').click(() => {
87 | let rect = NinePatchTrimming.detectRegion(this, this.editMode);
88 | if (!rect) {
89 | return;
90 | }
91 |
92 | if (this.editMode == 'stretch') {
93 | this.stretchRect = rect;
94 | } else if (this.editMode == 'opticalbounds') {
95 | this.opticalBoundsRect = rect;
96 | } else if (this.editMode == 'padding') {
97 | this.contentRect = rect;
98 | }
99 |
100 | this.saveRects();
101 | this.redrawOverlay();
102 | this.notifyChange_();
103 | });
104 | }
105 |
106 | setupDragging() {
107 | let mouseUpHandler_, draggingMouseMoveHandler_;
108 |
109 | let getEditRect_ = () => ({
110 | stretch: this.stretchRect,
111 | padding: this.contentRect,
112 | opticalbounds: this.opticalBoundsRect
113 | }[this.editMode]);
114 |
115 | this.$canvasContainer
116 | .on('mousedown', ev => {
117 | this.dragging = true;
118 | this.redrawOverlay();
119 | $(window)
120 | .on('mouseup', mouseUpHandler_)
121 | .on('mousemove', draggingMouseMoveHandler_);
122 | })
123 | .on('mousemove', ev => {
124 | if (!this.$imageCanvas) {
125 | return;
126 | }
127 |
128 | if (this.dragging) {
129 | return; // handled by other mousemove handler
130 | }
131 |
132 | let editRect = getEditRect_();
133 | let offs = this.$canvasContainer.offset();
134 | let offsetX = ev.pageX - offs.left;
135 | let offsetY = ev.pageY - offs.top;
136 |
137 | this.editLeft = this.editRight = this.editTop = this.editBottom = false;
138 |
139 | if (offsetX >= editRect.x * this.zoom - SLOP_PIXELS &&
140 | offsetX <= editRect.x * this.zoom + SLOP_PIXELS) {
141 | this.editLeft = true;
142 | } else if (offsetX >= (editRect.x + editRect.w) * this.zoom - SLOP_PIXELS &&
143 | offsetX <= (editRect.x + editRect.w) * this.zoom + SLOP_PIXELS) {
144 | this.editRight = true;
145 | }
146 |
147 | if (offsetY >= editRect.y * this.zoom - SLOP_PIXELS &&
148 | offsetY <= editRect.y * this.zoom + SLOP_PIXELS) {
149 | this.editTop = true;
150 | } else if (offsetY >= (editRect.y + editRect.h) * this.zoom - SLOP_PIXELS &&
151 | offsetY <= (editRect.y + editRect.h) * this.zoom + SLOP_PIXELS) {
152 | this.editBottom = true;
153 | }
154 |
155 | let cursor = 'default';
156 | if (this.editLeft) {
157 | if (this.editTop) {
158 | cursor = 'nw-resize';
159 | } else if (this.editBottom) {
160 | cursor = 'sw-resize';
161 | } else {
162 | cursor = 'w-resize';
163 | }
164 | } else if (this.editRight) {
165 | if (this.editTop) {
166 | cursor = 'ne-resize';
167 | } else if (this.editBottom) {
168 | cursor = 'se-resize';
169 | } else {
170 | cursor = 'e-resize';
171 | }
172 | } else if (this.editTop) {
173 | cursor = 'n-resize';
174 | } else if (this.editBottom) {
175 | cursor = 's-resize';
176 | }
177 | this.$canvasContainer.css('cursor', cursor);
178 | });
179 |
180 | mouseUpHandler_ = ev => {
181 | if (this.dragging) {
182 | this.dragging = false;
183 | this.redrawOverlay();
184 | this.saveRects();
185 | }
186 |
187 | $(window)
188 | .off('mousemove', draggingMouseMoveHandler_)
189 | .off('mouseup', mouseUpHandler_);
190 | };
191 |
192 | draggingMouseMoveHandler_ = ev => {
193 | ev.preventDefault();
194 | ev.stopPropagation();
195 |
196 | let editRect = getEditRect_();
197 | let offs = this.$canvasContainer.offset();
198 | let offsetX = ev.pageX - offs.left;
199 | let offsetY = ev.pageY - offs.top;
200 |
201 | if (this.editLeft) {
202 | let newX = Math.max(0, Math.min(editRect.x + editRect.w - 1, Math.round(offsetX / this.zoom)));
203 | editRect.w = editRect.w + editRect.x - newX;
204 | editRect.x = newX;
205 | }
206 | if (this.editTop) {
207 | let newY = Math.max(0, Math.min(editRect.y + editRect.h - 1, Math.round(offsetY / this.zoom)));
208 | editRect.h = editRect.h + editRect.y - newY;
209 | editRect.y = newY;
210 | }
211 | if (this.editRight) {
212 | editRect.w = Math.min(this.srcSize.w - editRect.x,
213 | Math.max(1, Math.round(offsetX / this.zoom) - editRect.x));
214 | }
215 | if (this.editBottom) {
216 | editRect.h = Math.min(this.srcSize.h - editRect.y,
217 | Math.max(1, Math.round(offsetY / this.zoom) - editRect.y));
218 | }
219 |
220 | this.redrawOverlay();
221 | this.notifyChange_();
222 | };
223 | }
224 |
225 | loadSourceImage(srcCtx, initRects = {}) {
226 | this.$canvasContainer.empty();
227 | $('.editor-button').attr('disabled', srcCtx ? null : 'disabled');
228 |
229 | if (!srcCtx) {
230 | return;
231 | }
232 |
233 | this.srcCtx = srcCtx;
234 |
235 | // Update the stage source size
236 | let srcSizeChanged = false;
237 | let newSrcSize = { w: this.srcCtx.canvas.width, h: this.srcCtx.canvas.height };
238 | srcSizeChanged = !this.srcSize
239 | || this.srcSize.w != newSrcSize.w
240 | || this.srcSize.h != newSrcSize.h;
241 | this.srcSize = newSrcSize;
242 |
243 | // Reset the stretch, padding/content, and optical bounds regions
244 | if (srcSizeChanged) {
245 | this.stretchRect = initRects.stretchRect || {
246 | x: Math.floor(this.srcSize.w / 3),
247 | y: Math.floor(this.srcSize.h / 3),
248 | w: Math.ceil(this.srcSize.w / 3),
249 | h: Math.ceil(this.srcSize.h / 3)
250 | };
251 |
252 | this.contentRect = initRects.contentRect || { x: 0, y: 0, w: this.srcSize.w, h: this.srcSize.h };
253 | this.opticalBoundsRect = initRects.opticalBoundsRect || { x: 0, y: 0, w: this.srcSize.w, h: this.srcSize.h };
254 | }
255 |
256 | if (!initRects.stretchRect) {
257 | this.loadLastRects();
258 | }
259 |
260 | // Create the stage canvas
261 | this.$imageCanvas = $('')
262 | .attr({
263 | width: this.srcSize.w,
264 | height: this.srcSize.h
265 | })
266 | .appendTo(this.$canvasContainer);
267 |
268 | this.$overlayCanvas = $('').addClass('overlay').appendTo(this.$canvasContainer);
269 |
270 | this.relayout();
271 | this.redrawImage();
272 | this.redrawOverlay();
273 | this.notifyChange_();
274 | }
275 |
276 | relayout() {
277 | if (!this.$imageCanvas) {
278 | return;
279 | }
280 |
281 | // Compute a zoom level that'll show the stage as large as possible
282 | let horizMaxZoom = Math.floor(this.$stage.width() / this.srcSize.w);
283 | let vertMaxZoom = Math.floor(this.$stage.height() / this.srcSize.h);
284 | this.zoom = Math.max(1, Math.min(horizMaxZoom, vertMaxZoom));
285 | this.zoomedSize = {
286 | w: this.srcSize.w * this.zoom,
287 | h: this.srcSize.h * this.zoom
288 | };
289 |
290 | this.$imageCanvas.css({
291 | width: this.zoomedSize.w,
292 | height: this.zoomedSize.h
293 | });
294 | this.$overlayCanvas.attr({
295 | width: this.zoomedSize.w,
296 | height: this.zoomedSize.h
297 | });
298 | }
299 |
300 | redrawImage() {
301 | if (!this.$imageCanvas) {
302 | return;
303 | }
304 |
305 | let imgCtx = this.$imageCanvas.get(0).getContext('2d');
306 | imgCtx.fillStyle = (this.matteColor == 'light') ? '#eee' : '#555';
307 | imgCtx.fillRect(0, 0, this.srcSize.w, this.srcSize.h);
308 |
309 | // draw source graphic
310 | imgCtx.drawImage(this.srcCtx.canvas, 0, 0);
311 | }
312 |
313 | redrawOverlay() {
314 | if (!this.srcCtx) {
315 | return;
316 | }
317 |
318 | let editRect = {
319 | stretch: this.stretchRect,
320 | padding: this.contentRect,
321 | opticalbounds: this.opticalBoundsRect
322 | }[this.editMode];
323 |
324 | let ctx = this.$overlayCanvas.get(0).getContext('2d');
325 | ctx.clearRect(0, 0, this.zoomedSize.w, this.zoomedSize.h);
326 | ctx.save();
327 |
328 | // draw current edit region
329 | if (editRect === this.stretchRect) {
330 | ctx.beginPath();
331 |
332 | ctx.moveTo(0, editRect.y * this.zoom + .5);
333 | ctx.lineTo(this.zoomedSize.w, editRect.y * this.zoom + .5);
334 |
335 | ctx.moveTo(0, (editRect.y + editRect.h) * this.zoom - .5);
336 | ctx.lineTo(this.zoomedSize.w, (editRect.y + editRect.h) * this.zoom - .5);
337 |
338 | ctx.moveTo(editRect.x * this.zoom + .5, 0);
339 | ctx.lineTo(editRect.x * this.zoom + .5, this.zoomedSize.h);
340 |
341 | ctx.moveTo((editRect.x + editRect.w) * this.zoom - .5, 0);
342 | ctx.lineTo((editRect.x + editRect.w) * this.zoom - .5, this.zoomedSize.h);
343 | } else {
344 | ctx.beginPath();
345 | ctx.rect(
346 | editRect.x * this.zoom + .5, editRect.y * this.zoom + .5,
347 | editRect.w * this.zoom - 1, editRect.h * this.zoom - 1);
348 | ctx.closePath();
349 | }
350 |
351 | if (this.dragging) {
352 | ctx.strokeStyle = 'rgba(255, 255, 255, 1)';
353 | ctx.lineWidth = 3;
354 | ctx.stroke();
355 | ctx.strokeStyle = 'rgba(255, 23, 68, 1)';
356 | ctx.lineWidth = 1;
357 | ctx.stroke();
358 | } else {
359 | ctx.strokeStyle = 'rgba(255, 255, 255, .5)';
360 | ctx.lineWidth = 3;
361 | ctx.stroke();
362 | ctx.strokeStyle = 'rgba(0, 0, 0, .5)';
363 | ctx.setLineDash([3, 3]);
364 | ctx.lineWidth = 1;
365 | ctx.stroke();
366 | }
367 |
368 | ctx.restore();
369 |
370 | // draw distance labels
371 | if (this.dragging) {
372 | let stageOffset = this.$canvasContainer.offset();
373 |
374 | this.$leftLabel
375 | .text(editRect.x)
376 | .css({
377 | left: stageOffset.left,
378 | width: editRect.x * this.zoom,
379 | top: stageOffset.top + (editRect.y + editRect.h / 2) * this.zoom
380 | })
381 | .show();
382 |
383 | this.$rightLabel
384 | .text(this.srcSize.w - editRect.x - editRect.w)
385 | .css({
386 | left: stageOffset.left + (editRect.x + editRect.w) * this.zoom,
387 | width: (this.srcSize.w - editRect.x - editRect.w) * this.zoom,
388 | top: stageOffset.top + (editRect.y + editRect.h / 2) * this.zoom
389 | })
390 | .show();
391 |
392 | this.$topLabel
393 | .text(editRect.y)
394 | .css({
395 | top: stageOffset.top,
396 | height: editRect.y * this.zoom,
397 | left: stageOffset.left + (editRect.x + editRect.w / 2) * this.zoom
398 | })
399 | .show();
400 |
401 | this.$bottomLabel
402 | .text(this.srcSize.h - editRect.y - editRect.h)
403 | .css({
404 | top: stageOffset.top + (editRect.y + editRect.h) * this.zoom,
405 | height: (this.srcSize.h - editRect.y - editRect.h) * this.zoom,
406 | left: stageOffset.left + (editRect.x + editRect.w / 2) * this.zoom
407 | })
408 | .show();
409 | } else {
410 | this.$topLabel.hide();
411 | this.$leftLabel.hide();
412 | this.$rightLabel.hide();
413 | this.$bottomLabel.hide();
414 | }
415 | }
416 |
417 | get localStorageKey() {
418 | return `assetStudioNinePatchStage-${this.name}`;
419 | }
420 |
421 | saveRects() {
422 | localStorage[this.localStorageKey] = JSON.stringify({
423 | stretchRect: this.stretchRect,
424 | contentRect: this.contentRect,
425 | opticalBoundsRect: this.opticalBoundsRect
426 | });
427 | }
428 |
429 | loadLastRects() {
430 | try {
431 | let store = JSON.parse(localStorage[this.localStorageKey]);
432 | if (store.stretchRect && store.contentRect && store.opticalBoundsRect) {
433 | this.stretchRect = fitRect_(store.stretchRect, this.srcSize);
434 | this.contentRect = fitRect_(store.contentRect, this.srcSize);
435 | this.opticalBoundsRect = fitRect_(store.opticalBoundsRect, this.srcSize);
436 | }
437 | } catch (e) {}
438 | }
439 | }
440 |
441 | function fitRect_(rect, size) {
442 | let newRect = {};
443 | newRect.x = Math.max(0, rect.x);
444 | newRect.y = Math.max(0, rect.y);
445 | newRect.w = Math.min(size.w - rect.x, rect.w);
446 | newRect.h = Math.min(size.h - rect.y, rect.h);
447 | return newRect;
448 | }
449 |
--------------------------------------------------------------------------------