1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | 'use strict';
|
11 |
|
12 | const Color = require('art/core/color');
|
13 | const Path = require('ARTSerializablePath');
|
14 | const Transform = require('art/core/transform');
|
15 |
|
16 | const React = require('React');
|
17 | const PropTypes = require('prop-types');
|
18 | const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
|
19 |
|
20 | const createReactNativeComponentClass = require('createReactNativeComponentClass');
|
21 | const merge = require('merge');
|
22 | const invariant = require('invariant');
|
23 |
|
24 |
|
25 |
|
26 | function arrayDiffer(a, b) {
|
27 | if (a == null || b == null) {
|
28 | return true;
|
29 | }
|
30 | if (a.length !== b.length) {
|
31 | return true;
|
32 | }
|
33 | for (let i = 0; i < a.length; i++) {
|
34 | if (a[i] !== b[i]) {
|
35 | return true;
|
36 | }
|
37 | }
|
38 | return false;
|
39 | }
|
40 |
|
41 | function fontAndLinesDiffer(a, b) {
|
42 | if (a === b) {
|
43 | return false;
|
44 | }
|
45 | if (a.font !== b.font) {
|
46 | if (a.font === null) {
|
47 | return true;
|
48 | }
|
49 | if (b.font === null) {
|
50 | return true;
|
51 | }
|
52 |
|
53 | if (
|
54 | a.font.fontFamily !== b.font.fontFamily ||
|
55 | a.font.fontSize !== b.font.fontSize ||
|
56 | a.font.fontWeight !== b.font.fontWeight ||
|
57 | a.font.fontStyle !== b.font.fontStyle
|
58 | ) {
|
59 | return true;
|
60 | }
|
61 | }
|
62 | return arrayDiffer(a.lines, b.lines);
|
63 | }
|
64 |
|
65 |
|
66 |
|
67 | const SurfaceViewAttributes = merge(ReactNativeViewAttributes.UIView, {
|
68 |
|
69 |
|
70 |
|
71 | });
|
72 |
|
73 | const NodeAttributes = {
|
74 | transform: {diff: arrayDiffer},
|
75 | opacity: true,
|
76 | };
|
77 |
|
78 | const GroupAttributes = merge(NodeAttributes, {
|
79 | clipping: {diff: arrayDiffer},
|
80 | });
|
81 |
|
82 | const RenderableAttributes = merge(NodeAttributes, {
|
83 | fill: {diff: arrayDiffer},
|
84 | stroke: {diff: arrayDiffer},
|
85 | strokeWidth: true,
|
86 | strokeCap: true,
|
87 | strokeJoin: true,
|
88 | strokeDash: {diff: arrayDiffer},
|
89 | });
|
90 |
|
91 | const ShapeAttributes = merge(RenderableAttributes, {
|
92 | d: {diff: arrayDiffer},
|
93 | });
|
94 |
|
95 | const TextAttributes = merge(RenderableAttributes, {
|
96 | alignment: true,
|
97 | frame: {diff: fontAndLinesDiffer},
|
98 | path: {diff: arrayDiffer},
|
99 | });
|
100 |
|
101 |
|
102 |
|
103 | const NativeSurfaceView = createReactNativeComponentClass(
|
104 | 'ARTSurfaceView',
|
105 | () => ({
|
106 | validAttributes: SurfaceViewAttributes,
|
107 | uiViewClassName: 'ARTSurfaceView',
|
108 | }),
|
109 | );
|
110 |
|
111 | const NativeGroup = createReactNativeComponentClass('ARTGroup', () => ({
|
112 | validAttributes: GroupAttributes,
|
113 | uiViewClassName: 'ARTGroup',
|
114 | }));
|
115 |
|
116 | const NativeShape = createReactNativeComponentClass('ARTShape', () => ({
|
117 | validAttributes: ShapeAttributes,
|
118 | uiViewClassName: 'ARTShape',
|
119 | }));
|
120 |
|
121 | const NativeText = createReactNativeComponentClass('ARTText', () => ({
|
122 | validAttributes: TextAttributes,
|
123 | uiViewClassName: 'ARTText',
|
124 | }));
|
125 |
|
126 |
|
127 |
|
128 | function childrenAsString(children) {
|
129 | if (!children) {
|
130 | return '';
|
131 | }
|
132 | if (typeof children === 'string') {
|
133 | return children;
|
134 | }
|
135 | if (children.length) {
|
136 | return children.join('\n');
|
137 | }
|
138 | return '';
|
139 | }
|
140 |
|
141 |
|
142 |
|
143 | class Surface extends React.Component {
|
144 | static childContextTypes = {
|
145 | isInSurface: PropTypes.bool,
|
146 | };
|
147 |
|
148 | getChildContext() {
|
149 | return {isInSurface: true};
|
150 | }
|
151 |
|
152 | render() {
|
153 | const height = extractNumber(this.props.height, 0);
|
154 | const width = extractNumber(this.props.width, 0);
|
155 |
|
156 | return (
|
157 | <NativeSurfaceView style={[this.props.style, {height, width}]}>
|
158 | {this.props.children}
|
159 | </NativeSurfaceView>
|
160 | );
|
161 | }
|
162 | }
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 | function extractNumber(value, defaultValue) {
|
170 | if (value == null) {
|
171 | return defaultValue;
|
172 | }
|
173 | return +value;
|
174 | }
|
175 |
|
176 | const pooledTransform = new Transform();
|
177 |
|
178 | function extractTransform(props) {
|
179 | const scaleX =
|
180 | props.scaleX != null ? props.scaleX : props.scale != null ? props.scale : 1;
|
181 | const scaleY =
|
182 | props.scaleY != null ? props.scaleY : props.scale != null ? props.scale : 1;
|
183 |
|
184 | pooledTransform
|
185 | .transformTo(1, 0, 0, 1, 0, 0)
|
186 | .move(props.x || 0, props.y || 0)
|
187 | .rotate(props.rotation || 0, props.originX, props.originY)
|
188 | .scale(scaleX, scaleY, props.originX, props.originY);
|
189 |
|
190 | if (props.transform != null) {
|
191 | pooledTransform.transform(props.transform);
|
192 | }
|
193 |
|
194 | return [
|
195 | pooledTransform.xx,
|
196 | pooledTransform.yx,
|
197 | pooledTransform.xy,
|
198 | pooledTransform.yy,
|
199 | pooledTransform.x,
|
200 | pooledTransform.y,
|
201 | ];
|
202 | }
|
203 |
|
204 | function extractOpacity(props) {
|
205 |
|
206 | if (props.visible === false) {
|
207 | return 0;
|
208 | }
|
209 | if (props.opacity == null) {
|
210 | return 1;
|
211 | }
|
212 | return +props.opacity;
|
213 | }
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | class Group extends React.Component {
|
221 | static contextTypes = {
|
222 | isInSurface: PropTypes.bool.isRequired,
|
223 | };
|
224 |
|
225 | render() {
|
226 | const props = this.props;
|
227 | invariant(
|
228 | this.context.isInSurface,
|
229 | 'ART: <Group /> must be a child of a <Surface />',
|
230 | );
|
231 | return (
|
232 | <NativeGroup
|
233 | opacity={extractOpacity(props)}
|
234 | transform={extractTransform(props)}>
|
235 | {this.props.children}
|
236 | </NativeGroup>
|
237 | );
|
238 | }
|
239 | }
|
240 |
|
241 | class ClippingRectangle extends React.Component {
|
242 | render() {
|
243 | const props = this.props;
|
244 | const x = extractNumber(props.x, 0);
|
245 | const y = extractNumber(props.y, 0);
|
246 | const w = extractNumber(props.width, 0);
|
247 | const h = extractNumber(props.height, 0);
|
248 | const clipping = [x, y, w, h];
|
249 |
|
250 | const propsExcludingXAndY = merge(props);
|
251 | delete propsExcludingXAndY.x;
|
252 | delete propsExcludingXAndY.y;
|
253 | return (
|
254 | <NativeGroup
|
255 | clipping={clipping}
|
256 | opacity={extractOpacity(props)}
|
257 | transform={extractTransform(propsExcludingXAndY)}>
|
258 | {this.props.children}
|
259 | </NativeGroup>
|
260 | );
|
261 | }
|
262 | }
|
263 |
|
264 |
|
265 |
|
266 | const SOLID_COLOR = 0;
|
267 | const LINEAR_GRADIENT = 1;
|
268 | const RADIAL_GRADIENT = 2;
|
269 | const PATTERN = 3;
|
270 |
|
271 | function insertColorIntoArray(color, targetArray, atIndex) {
|
272 | const c = new Color(color);
|
273 | targetArray[atIndex + 0] = c.red / 255;
|
274 | targetArray[atIndex + 1] = c.green / 255;
|
275 | targetArray[atIndex + 2] = c.blue / 255;
|
276 | targetArray[atIndex + 3] = c.alpha;
|
277 | }
|
278 |
|
279 | function insertColorsIntoArray(stops, targetArray, atIndex) {
|
280 | let i = 0;
|
281 | if ('length' in stops) {
|
282 | while (i < stops.length) {
|
283 | insertColorIntoArray(stops[i], targetArray, atIndex + i * 4);
|
284 | i++;
|
285 | }
|
286 | } else {
|
287 | for (const offset in stops) {
|
288 | insertColorIntoArray(stops[offset], targetArray, atIndex + i * 4);
|
289 | i++;
|
290 | }
|
291 | }
|
292 | return atIndex + i * 4;
|
293 | }
|
294 |
|
295 | function insertOffsetsIntoArray(stops, targetArray, atIndex, multi, reverse) {
|
296 | let offsetNumber;
|
297 | let i = 0;
|
298 | if ('length' in stops) {
|
299 | while (i < stops.length) {
|
300 | offsetNumber = (i / (stops.length - 1)) * multi;
|
301 | targetArray[atIndex + i] = reverse ? 1 - offsetNumber : offsetNumber;
|
302 | i++;
|
303 | }
|
304 | } else {
|
305 | for (const offsetString in stops) {
|
306 | offsetNumber = +offsetString * multi;
|
307 | targetArray[atIndex + i] = reverse ? 1 - offsetNumber : offsetNumber;
|
308 | i++;
|
309 | }
|
310 | }
|
311 | return atIndex + i;
|
312 | }
|
313 |
|
314 | function insertColorStopsIntoArray(stops, targetArray, atIndex) {
|
315 | const lastIndex = insertColorsIntoArray(stops, targetArray, atIndex);
|
316 | insertOffsetsIntoArray(stops, targetArray, lastIndex, 1, false);
|
317 | }
|
318 |
|
319 | function insertDoubleColorStopsIntoArray(stops, targetArray, atIndex) {
|
320 | let lastIndex = insertColorsIntoArray(stops, targetArray, atIndex);
|
321 | lastIndex = insertColorsIntoArray(stops, targetArray, lastIndex);
|
322 | lastIndex = insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, false);
|
323 | insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, true);
|
324 | }
|
325 |
|
326 | function applyBoundingBoxToBrushData(brushData, props) {
|
327 | const type = brushData[0];
|
328 | const width = +props.width;
|
329 | const height = +props.height;
|
330 | if (type === LINEAR_GRADIENT) {
|
331 | brushData[1] *= width;
|
332 | brushData[2] *= height;
|
333 | brushData[3] *= width;
|
334 | brushData[4] *= height;
|
335 | } else if (type === RADIAL_GRADIENT) {
|
336 | brushData[1] *= width;
|
337 | brushData[2] *= height;
|
338 | brushData[3] *= width;
|
339 | brushData[4] *= height;
|
340 | brushData[5] *= width;
|
341 | brushData[6] *= height;
|
342 | } else if (type === PATTERN) {
|
343 |
|
344 | }
|
345 | }
|
346 |
|
347 | function extractBrush(colorOrBrush, props) {
|
348 | if (colorOrBrush == null) {
|
349 | return null;
|
350 | }
|
351 | if (colorOrBrush._brush) {
|
352 | if (colorOrBrush._bb) {
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 | applyBoundingBoxToBrushData(colorOrBrush._brush, props);
|
359 | colorOrBrush._bb = false;
|
360 | }
|
361 | return colorOrBrush._brush;
|
362 | }
|
363 | const c = new Color(colorOrBrush);
|
364 | return [SOLID_COLOR, c.red / 255, c.green / 255, c.blue / 255, c.alpha];
|
365 | }
|
366 |
|
367 | function extractColor(color) {
|
368 | if (color == null) {
|
369 | return null;
|
370 | }
|
371 | const c = new Color(color);
|
372 | return [c.red / 255, c.green / 255, c.blue / 255, c.alpha];
|
373 | }
|
374 |
|
375 | function extractStrokeCap(strokeCap) {
|
376 | switch (strokeCap) {
|
377 | case 'butt':
|
378 | return 0;
|
379 | case 'square':
|
380 | return 2;
|
381 | default:
|
382 | return 1;
|
383 | }
|
384 | }
|
385 |
|
386 | function extractStrokeJoin(strokeJoin) {
|
387 | switch (strokeJoin) {
|
388 | case 'miter':
|
389 | return 0;
|
390 | case 'bevel':
|
391 | return 2;
|
392 | default:
|
393 | return 1;
|
394 | }
|
395 | }
|
396 |
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 | export type ShapeProps = {|
|
403 | fill?: mixed,
|
404 | stroke?: mixed,
|
405 | strokeCap?: mixed,
|
406 | strokeDash?: mixed,
|
407 | strokeJoin?: mixed,
|
408 | strokeWidth?: mixed,
|
409 | x?: number,
|
410 | y?: number,
|
411 | opacity?: mixed,
|
412 | |};
|
413 |
|
414 | class Shape extends React.Component<ShapeProps> {
|
415 | render() {
|
416 | const props = this.props;
|
417 | const path = props.d || childrenAsString(props.children);
|
418 | const d = (path instanceof Path ? path : new Path(path)).toJSON();
|
419 | return (
|
420 | <NativeShape
|
421 | fill={extractBrush(props.fill, props)}
|
422 | opacity={extractOpacity(props)}
|
423 | stroke={extractColor(props.stroke)}
|
424 | strokeCap={extractStrokeCap(props.strokeCap)}
|
425 | strokeDash={props.strokeDash || null}
|
426 | strokeJoin={extractStrokeJoin(props.strokeJoin)}
|
427 | strokeWidth={extractNumber(props.strokeWidth, 1)}
|
428 | transform={extractTransform(props)}
|
429 | d={d}
|
430 | />
|
431 | );
|
432 | }
|
433 | }
|
434 |
|
435 |
|
436 |
|
437 | const cachedFontObjectsFromString = {};
|
438 |
|
439 | const fontFamilyPrefix = /^[\s"']*/;
|
440 | const fontFamilySuffix = /[\s"']*$/;
|
441 |
|
442 | function extractSingleFontFamily(fontFamilyString) {
|
443 |
|
444 |
|
445 |
|
446 | return fontFamilyString
|
447 | .split(',')[0]
|
448 | .replace(fontFamilyPrefix, '')
|
449 | .replace(fontFamilySuffix, '');
|
450 | }
|
451 |
|
452 | function parseFontString(font) {
|
453 | if (cachedFontObjectsFromString.hasOwnProperty(font)) {
|
454 | return cachedFontObjectsFromString[font];
|
455 | }
|
456 | const regexp = /^\s*((?:(?:normal|bold|italic)\s+)*)(?:(\d+(?:\.\d+)?)[ptexm\%]*(?:\s*\/.*?)?\s+)?\s*\"?([^\"]*)/i;
|
457 | const match = regexp.exec(font);
|
458 | if (!match) {
|
459 | return null;
|
460 | }
|
461 | const fontFamily = extractSingleFontFamily(match[3]);
|
462 | const fontSize = +match[2] || 12;
|
463 | const isBold = /bold/.exec(match[1]);
|
464 | const isItalic = /italic/.exec(match[1]);
|
465 | cachedFontObjectsFromString[font] = {
|
466 | fontFamily: fontFamily,
|
467 | fontSize: fontSize,
|
468 | fontWeight: isBold ? 'bold' : 'normal',
|
469 | fontStyle: isItalic ? 'italic' : 'normal',
|
470 | };
|
471 | return cachedFontObjectsFromString[font];
|
472 | }
|
473 |
|
474 | function extractFont(font) {
|
475 | if (font == null) {
|
476 | return null;
|
477 | }
|
478 | if (typeof font === 'string') {
|
479 | return parseFontString(font);
|
480 | }
|
481 | const fontFamily = extractSingleFontFamily(font.fontFamily);
|
482 | const fontSize = +font.fontSize || 12;
|
483 | const fontWeight =
|
484 | font.fontWeight != null ? font.fontWeight.toString() : '400';
|
485 | return {
|
486 |
|
487 | fontFamily: fontFamily,
|
488 | fontSize: fontSize,
|
489 | fontWeight: fontWeight,
|
490 | fontStyle: font.fontStyle,
|
491 | };
|
492 | }
|
493 |
|
494 | const newLine = /\n/g;
|
495 | function extractFontAndLines(font, text) {
|
496 | return {font: extractFont(font), lines: text.split(newLine)};
|
497 | }
|
498 |
|
499 | function extractAlignment(alignment) {
|
500 | switch (alignment) {
|
501 | case 'right':
|
502 | return 1;
|
503 | case 'center':
|
504 | return 2;
|
505 | default:
|
506 | return 0;
|
507 | }
|
508 | }
|
509 |
|
510 | class Text extends React.Component {
|
511 | render() {
|
512 | const props = this.props;
|
513 | const path = props.path;
|
514 | const textPath = path
|
515 | ? (path instanceof Path ? path : new Path(path)).toJSON()
|
516 | : null;
|
517 | const textFrame = extractFontAndLines(
|
518 | props.font,
|
519 | childrenAsString(props.children),
|
520 | );
|
521 | return (
|
522 | <NativeText
|
523 | fill={extractBrush(props.fill, props)}
|
524 | opacity={extractOpacity(props)}
|
525 | stroke={extractColor(props.stroke)}
|
526 | strokeCap={extractStrokeCap(props.strokeCap)}
|
527 | strokeDash={props.strokeDash || null}
|
528 | strokeJoin={extractStrokeJoin(props.strokeJoin)}
|
529 | strokeWidth={extractNumber(props.strokeWidth, 1)}
|
530 | transform={extractTransform(props)}
|
531 | alignment={extractAlignment(props.alignment)}
|
532 | frame={textFrame}
|
533 | path={textPath}
|
534 | />
|
535 | );
|
536 | }
|
537 | }
|
538 |
|
539 |
|
540 |
|
541 | function LinearGradient(stops, x1, y1, x2, y2) {
|
542 | const type = LINEAR_GRADIENT;
|
543 |
|
544 | if (arguments.length < 5) {
|
545 | const angle = ((x1 == null ? 270 : x1) * Math.PI) / 180;
|
546 |
|
547 | let x = Math.cos(angle);
|
548 | let y = -Math.sin(angle);
|
549 | const l = (Math.abs(x) + Math.abs(y)) / 2;
|
550 |
|
551 | x *= l;
|
552 | y *= l;
|
553 |
|
554 | x1 = 0.5 - x;
|
555 | x2 = 0.5 + x;
|
556 | y1 = 0.5 - y;
|
557 | y2 = 0.5 + y;
|
558 | this._bb = true;
|
559 | } else {
|
560 | this._bb = false;
|
561 | }
|
562 |
|
563 | const brushData = [type, +x1, +y1, +x2, +y2];
|
564 | insertColorStopsIntoArray(stops, brushData, 5);
|
565 | this._brush = brushData;
|
566 | }
|
567 |
|
568 | function RadialGradient(stops, fx, fy, rx, ry, cx, cy) {
|
569 | if (ry == null) {
|
570 | ry = rx;
|
571 | }
|
572 | if (cx == null) {
|
573 | cx = fx;
|
574 | }
|
575 | if (cy == null) {
|
576 | cy = fy;
|
577 | }
|
578 | if (fx == null) {
|
579 |
|
580 |
|
581 | fx = fy = rx = ry = cx = cy = 0.5;
|
582 | this._bb = true;
|
583 | } else {
|
584 | this._bb = false;
|
585 | }
|
586 |
|
587 |
|
588 |
|
589 |
|
590 | const brushData = [RADIAL_GRADIENT, +fx, +fy, +rx * 2, +ry * 2, +cx, +cy];
|
591 | insertDoubleColorStopsIntoArray(stops, brushData, 7);
|
592 | this._brush = brushData;
|
593 | }
|
594 |
|
595 | function Pattern(url, width, height, left, top) {
|
596 | this._brush = [PATTERN, url, +left || 0, +top || 0, +width, +height];
|
597 | }
|
598 |
|
599 | const ReactART = {
|
600 | LinearGradient: LinearGradient,
|
601 | RadialGradient: RadialGradient,
|
602 | Pattern: Pattern,
|
603 | Transform: Transform,
|
604 | Path: Path,
|
605 | Surface: Surface,
|
606 | Group: Group,
|
607 | ClippingRectangle: ClippingRectangle,
|
608 | Shape: Shape,
|
609 | Text: Text,
|
610 | };
|
611 |
|
612 | module.exports = ReactART;
|