@ray-js/graffiti 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/lib/components/index.js +200 -0
- package/lib/components/index.json +3 -0
- package/lib/components/index.less +0 -0
- package/lib/components/index.rjs +165 -0
- package/lib/components/index.tyml +6 -0
- package/lib/index.config.js +14 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +59 -0
- package/lib/props.d.ts +127 -0
- package/lib/props.js +20 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +8 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
English | [简体中文](./README-zh_CN.md)
|
|
2
|
+
|
|
3
|
+
# @ray-js/graffiti
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@ray-js/graffiti) [](https://www.npmjs.com/package/@ray-js/graffiti)
|
|
6
|
+
|
|
7
|
+
> Canvas Graffiti
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
$ npm install @ray-js/graffiti
|
|
13
|
+
# or
|
|
14
|
+
$ yarn add @ray-js/graffiti
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Develop
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# install deps
|
|
21
|
+
yarn
|
|
22
|
+
|
|
23
|
+
# watch compile demo
|
|
24
|
+
yarn start:tuya
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Code Demonstration
|
|
28
|
+
|
|
29
|
+
### Basic usage
|
|
30
|
+
|
|
31
|
+
- By changing the operation type, you can switch to pencil mode, eraser mode, paint bucket mode
|
|
32
|
+
- By changing `penColor`, you can pass in different pen colors
|
|
33
|
+
- `needStroke` is `true`, start monitoring each brush data, and get the x, y coordinate path data of each brush stroke through `onStrokeChange`
|
|
34
|
+
- By updating the value of `saveTrigger`, you can trigger canvas saving and return the base64 data of the canvas
|
|
35
|
+
- By updating the value of `clearTrigger`, you can trigger canvas clearing
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import React, { useState } from 'react';
|
|
39
|
+
import { View } from '@ray-js/ray';
|
|
40
|
+
import Graffiti from '@ray-js/graffiti';
|
|
41
|
+
|
|
42
|
+
type IStrokeData = {
|
|
43
|
+
points: Array<{ x: number; y: number }>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type IData = {
|
|
47
|
+
base64: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function Home() {
|
|
51
|
+
const [actionType, setActiontype] = useState<'pencil' | 'eraser' | 'paint'>('pencil');
|
|
52
|
+
const [color, setColor] = useState('rgba(255, 0, 0, 1)');
|
|
53
|
+
const [saveTrigger, setSaveTrigger] = useState(0);
|
|
54
|
+
const [clearTrigger, setClearTrigger] = useState(0);
|
|
55
|
+
|
|
56
|
+
const reset = () => {
|
|
57
|
+
setClearTrigger(clearTrigger + 1);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const save = () => {
|
|
61
|
+
setSaveTrigger(saveTrigger + 1);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleStrokeChange = (data: IPoints) => {
|
|
65
|
+
console.log('handleStrokeChange', data);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleSaveData = (data: IData) => {
|
|
69
|
+
console.log('handleSaveData', data);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
<Graffiti
|
|
75
|
+
needStroke
|
|
76
|
+
penColor={color}
|
|
77
|
+
actionType={actionType}
|
|
78
|
+
saveTrigger={saveTrigger}
|
|
79
|
+
clearTrigger={clearTrigger}
|
|
80
|
+
onStrokeChange={handleStrokeChange}
|
|
81
|
+
onSaveData={handleSaveData}
|
|
82
|
+
/>
|
|
83
|
+
<View className="footer">
|
|
84
|
+
<Button className="btn" type="primary" onClick={reset}>
|
|
85
|
+
Reset
|
|
86
|
+
</Button>
|
|
87
|
+
<Button className="btn" type="primary" onClick={save}>
|
|
88
|
+
Save
|
|
89
|
+
</Button>
|
|
90
|
+
</View>
|
|
91
|
+
<>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import Render from './index.rjs';
|
|
2
|
+
import { getSystemInfoSync } from '@ray-js/ray';
|
|
3
|
+
let _systemInfoResult = null;
|
|
4
|
+
export const getSystemInfoResult = () => {
|
|
5
|
+
if (_systemInfoResult) {
|
|
6
|
+
return _systemInfoResult;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const info = getSystemInfoSync();
|
|
10
|
+
_systemInfoResult = info;
|
|
11
|
+
return _systemInfoResult;
|
|
12
|
+
} catch (err) {
|
|
13
|
+
return {
|
|
14
|
+
windowHeight: 667,
|
|
15
|
+
windowWidth: 375,
|
|
16
|
+
pixelRatio: 2
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// 获取真实的px
|
|
22
|
+
export const getDeviceRealPx = px => {
|
|
23
|
+
const info = getSystemInfoResult();
|
|
24
|
+
return Math.round(px * (info.windowWidth / 375));
|
|
25
|
+
};
|
|
26
|
+
const WIDTH = 353;
|
|
27
|
+
const HEIGHT = 353;
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line no-undef
|
|
30
|
+
Component({
|
|
31
|
+
properties: {
|
|
32
|
+
canvasIdPrefix: {
|
|
33
|
+
type: String,
|
|
34
|
+
value: 'ray-graffiti' // canvas
|
|
35
|
+
},
|
|
36
|
+
width: {
|
|
37
|
+
type: Number,
|
|
38
|
+
value: WIDTH // 画布宽度
|
|
39
|
+
},
|
|
40
|
+
height: {
|
|
41
|
+
type: Number,
|
|
42
|
+
value: HEIGHT // 画布高度
|
|
43
|
+
},
|
|
44
|
+
mode: {
|
|
45
|
+
type: String,
|
|
46
|
+
value: 'grid' // grid 按方格数显示长宽相同, pixel 按像素显示
|
|
47
|
+
},
|
|
48
|
+
gridSizeX: {
|
|
49
|
+
type: Number,
|
|
50
|
+
value: 32 // 每行格子数量
|
|
51
|
+
},
|
|
52
|
+
gridSizeY: {
|
|
53
|
+
type: Number,
|
|
54
|
+
value: 32 // 每列格子数量
|
|
55
|
+
},
|
|
56
|
+
pixelSizeX: {
|
|
57
|
+
type: Number,
|
|
58
|
+
value: 10 // 格子宽度
|
|
59
|
+
},
|
|
60
|
+
pixelSizeY: {
|
|
61
|
+
type: Number,
|
|
62
|
+
value: 10 // 格子高度
|
|
63
|
+
},
|
|
64
|
+
pixelGap: {
|
|
65
|
+
type: Number,
|
|
66
|
+
value: 1 // 格子间距
|
|
67
|
+
},
|
|
68
|
+
pixelShape: {
|
|
69
|
+
type: String,
|
|
70
|
+
value: 'square' // 格子形状, square方形, circle圆形
|
|
71
|
+
},
|
|
72
|
+
pixelColor: {
|
|
73
|
+
type: String,
|
|
74
|
+
value: 'rgba(255, 255, 255, 0.15)' // 格子颜色
|
|
75
|
+
},
|
|
76
|
+
penColor: {
|
|
77
|
+
// 画笔颜色
|
|
78
|
+
type: String,
|
|
79
|
+
value: 'rgb(255, 255, 255)',
|
|
80
|
+
observer(newValue) {
|
|
81
|
+
if (newValue && this.render) {
|
|
82
|
+
if (this.data.actionType === 'pencil') {
|
|
83
|
+
this.render.updateColor(newValue);
|
|
84
|
+
} else if (this.data.actionType === 'paint') {
|
|
85
|
+
this.render.changeBg(newValue);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
actionType: {
|
|
91
|
+
// 操作类型 pencil画笔, eraser橡皮擦, paint油漆桶
|
|
92
|
+
type: String,
|
|
93
|
+
value: 'pencil',
|
|
94
|
+
observer(newValue) {
|
|
95
|
+
if (newValue && this.render) {
|
|
96
|
+
if (newValue === 'eraser') {
|
|
97
|
+
this.render.updateColor(this.data.pixelColor);
|
|
98
|
+
} else if (newValue === 'pencil') {
|
|
99
|
+
this.render.updateColor(this.data.penColor);
|
|
100
|
+
} else if (newValue === 'paint') {
|
|
101
|
+
this.render.changeBg(this.data.penColor);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
needStroke: {
|
|
107
|
+
// 是否需要每一画笔数据
|
|
108
|
+
type: Boolean,
|
|
109
|
+
value: false
|
|
110
|
+
},
|
|
111
|
+
saveTrigger: {
|
|
112
|
+
// 保存标识
|
|
113
|
+
type: Number,
|
|
114
|
+
value: 0,
|
|
115
|
+
observer(newValue) {
|
|
116
|
+
if (newValue && this.render) {
|
|
117
|
+
this.render.save();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
clearTrigger: {
|
|
122
|
+
// 清空标识
|
|
123
|
+
type: Number,
|
|
124
|
+
value: 0,
|
|
125
|
+
observer(newValue) {
|
|
126
|
+
if (newValue && this.render) {
|
|
127
|
+
this.render.clear();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
data: {
|
|
133
|
+
realWidth: WIDTH,
|
|
134
|
+
realHeight: HEIGHT
|
|
135
|
+
},
|
|
136
|
+
lifetimes: {
|
|
137
|
+
created() {
|
|
138
|
+
this.render = new Render(this);
|
|
139
|
+
},
|
|
140
|
+
ready() {
|
|
141
|
+
let {
|
|
142
|
+
canvasIdPrefix,
|
|
143
|
+
width,
|
|
144
|
+
height,
|
|
145
|
+
gridSizeX,
|
|
146
|
+
gridSizeY,
|
|
147
|
+
pixelSizeX,
|
|
148
|
+
pixelSizeY,
|
|
149
|
+
pixelGap,
|
|
150
|
+
mode
|
|
151
|
+
} = this.data;
|
|
152
|
+
width = getDeviceRealPx(width);
|
|
153
|
+
height = getDeviceRealPx(height);
|
|
154
|
+
pixelSizeX = getDeviceRealPx(pixelSizeX);
|
|
155
|
+
pixelSizeY = getDeviceRealPx(pixelSizeY);
|
|
156
|
+
pixelGap = getDeviceRealPx(pixelGap);
|
|
157
|
+
let realPixelSizeX = pixelSizeX;
|
|
158
|
+
let realPixelSizeY = pixelSizeY;
|
|
159
|
+
if (mode === 'grid') {
|
|
160
|
+
realPixelSizeX = Math.floor((width - pixelGap) / gridSizeX - pixelGap);
|
|
161
|
+
const gridModeSizeX = (realPixelSizeX + pixelGap) * gridSizeX + pixelGap;
|
|
162
|
+
realPixelSizeY = Math.floor((height - pixelGap) / gridSizeY - pixelGap);
|
|
163
|
+
const gridModeSizeY = (realPixelSizeY + pixelGap) * gridSizeY + pixelGap;
|
|
164
|
+
this.setData({
|
|
165
|
+
realWidth: gridModeSizeX,
|
|
166
|
+
realHeight: gridModeSizeY
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
this.setData({
|
|
170
|
+
realWidth: width,
|
|
171
|
+
realHeight: height
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
this.render.initPanel({
|
|
175
|
+
canvasIdPrefix: canvasIdPrefix,
|
|
176
|
+
width: width,
|
|
177
|
+
height: height,
|
|
178
|
+
mode: this.data.mode,
|
|
179
|
+
gridSizeX: gridSizeX,
|
|
180
|
+
gridSizeY: gridSizeY,
|
|
181
|
+
pixelSizeX: realPixelSizeX,
|
|
182
|
+
pixelSizeY: realPixelSizeY,
|
|
183
|
+
pixelGap: pixelGap,
|
|
184
|
+
pixelShape: this.data.pixelShape,
|
|
185
|
+
pixelColor: this.data.pixelColor,
|
|
186
|
+
penColor: this.data.penColor
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
methods: {
|
|
191
|
+
touchend(data) {
|
|
192
|
+
if (!this.data.needStroke) return;
|
|
193
|
+
if (this.data.actionType === 'paint') return;
|
|
194
|
+
this.triggerEvent('strokeChange', data);
|
|
195
|
+
},
|
|
196
|
+
genImageData(data) {
|
|
197
|
+
this.triggerEvent('saveData', data);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const pixelRatio = Math.floor(getSystemInfo().pixelRatio) || 1; // 分辨率, 整数
|
|
2
|
+
|
|
3
|
+
export default Render({
|
|
4
|
+
async initPanel({
|
|
5
|
+
canvasIdPrefix,
|
|
6
|
+
width,
|
|
7
|
+
height,
|
|
8
|
+
mode,
|
|
9
|
+
gridSizeX,
|
|
10
|
+
gridSizeY,
|
|
11
|
+
pixelSizeX,
|
|
12
|
+
pixelSizeY,
|
|
13
|
+
pixelGap,
|
|
14
|
+
pixelShape,
|
|
15
|
+
pixelColor,
|
|
16
|
+
penColor,
|
|
17
|
+
}) {
|
|
18
|
+
let canvas = await getCanvasById(`${canvasIdPrefix}-sourceCanvas`);
|
|
19
|
+
|
|
20
|
+
// 根据屏幕分辨率动态计算canvas尺寸
|
|
21
|
+
if (mode === 'grid') {
|
|
22
|
+
const gridModeSizeX = (pixelSizeX + pixelGap) * gridSizeX + pixelGap;
|
|
23
|
+
const gridModeSizeY = (pixelSizeY + pixelGap) * gridSizeY + pixelGap;
|
|
24
|
+
canvas.width = gridModeSizeX * pixelRatio;
|
|
25
|
+
canvas.height = gridModeSizeY * pixelRatio;
|
|
26
|
+
canvas.style.width = gridModeSizeX + 'px';
|
|
27
|
+
canvas.style.height = gridModeSizeY + 'px';
|
|
28
|
+
} else {
|
|
29
|
+
canvas.width = width * pixelRatio;
|
|
30
|
+
canvas.height = height * pixelRatio;
|
|
31
|
+
canvas.style.width = `${width}px`;
|
|
32
|
+
canvas.style.height = `${height}px`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ctx = canvas.getContext('2d');
|
|
36
|
+
ctx.scale(pixelRatio, pixelRatio);
|
|
37
|
+
this.canvas = canvas;
|
|
38
|
+
this.ctx = ctx;
|
|
39
|
+
|
|
40
|
+
this.mode = mode;
|
|
41
|
+
this.width = width;
|
|
42
|
+
this.height = height;
|
|
43
|
+
this.gridSizeX = gridSizeX;
|
|
44
|
+
this.gridSizeY = gridSizeY;
|
|
45
|
+
this.pixelSizeX = pixelSizeX;
|
|
46
|
+
this.pixelSizeY = pixelSizeY;
|
|
47
|
+
this.pixelGap = pixelGap;
|
|
48
|
+
this.pixelShape = pixelShape;
|
|
49
|
+
this.pixelColor = pixelColor;
|
|
50
|
+
this.penColor = penColor;
|
|
51
|
+
this.canvasIdPrefix = canvasIdPrefix;
|
|
52
|
+
// 用于存储触摸开始到结束经过的方格坐标数组集合
|
|
53
|
+
this.touchedSquaresSet = new Set();
|
|
54
|
+
// 记录触摸是否开始
|
|
55
|
+
this.isTouchStarted = false;
|
|
56
|
+
|
|
57
|
+
// 初始化画布, 绘制像素点
|
|
58
|
+
this.createPixel(pixelColor);
|
|
59
|
+
|
|
60
|
+
canvas.addEventListener('touchstart', this.handleTouchstart, false);
|
|
61
|
+
canvas.addEventListener('touchmove', this.handleTouchmove, false);
|
|
62
|
+
canvas.addEventListener('touchend', this.handleTouchend, false);
|
|
63
|
+
|
|
64
|
+
ctx.imageSmoothingEnabled = true; // 开启抗锯齿
|
|
65
|
+
ctx.imageSmoothingQuality = 'high'; // 高质量抗锯齿
|
|
66
|
+
},
|
|
67
|
+
handleTouchstart(e) {
|
|
68
|
+
const { canvas, pixelGap, pixelSizeX, pixelSizeY } = this;
|
|
69
|
+
this.touchedSquaresSet.clear();
|
|
70
|
+
this.isTouchStarted = true;
|
|
71
|
+
const touch = e.changedTouches[0];
|
|
72
|
+
const rect = canvas.getBoundingClientRect();
|
|
73
|
+
const x = Math.floor((touch.pageX - rect.left - pixelGap) / (pixelSizeX + pixelGap));
|
|
74
|
+
const y = Math.floor((touch.pageY - rect.top - pixelGap) / (pixelSizeY + pixelGap));
|
|
75
|
+
const coordinate = `${x},${y}`;
|
|
76
|
+
if (!this.touchedSquaresSet.has(coordinate)) {
|
|
77
|
+
this.touchedSquaresSet.add(coordinate);
|
|
78
|
+
this.fillPixel(x, y, this.penColor);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
handleTouchmove(e) {
|
|
82
|
+
const { canvas, pixelGap, pixelSizeX, pixelSizeY } = this;
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
if (this.isTouchStarted) {
|
|
85
|
+
const touch = e.changedTouches[0];
|
|
86
|
+
const rect = canvas.getBoundingClientRect();
|
|
87
|
+
const x = Math.floor((touch.pageX - rect.left - pixelGap) / (pixelSizeX + pixelGap));
|
|
88
|
+
const y = Math.floor((touch.pageY - rect.top - pixelGap) / (pixelSizeY + pixelGap));
|
|
89
|
+
const coordinate = `${x},${y}`;
|
|
90
|
+
if (!this.touchedSquaresSet.has(coordinate)) {
|
|
91
|
+
this.touchedSquaresSet.add(coordinate);
|
|
92
|
+
this.fillPixel(x, y, this.penColor);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
handleTouchend() {
|
|
97
|
+
this.isTouchStarted = false;
|
|
98
|
+
const touchedSquares = [];
|
|
99
|
+
for (const coordinateStr of this.touchedSquaresSet) {
|
|
100
|
+
const [x, y] = coordinateStr.split(',');
|
|
101
|
+
touchedSquares.push({ x: Number(x), y: Number(y) });
|
|
102
|
+
}
|
|
103
|
+
this.callMethod('touchend', { points: touchedSquares });
|
|
104
|
+
},
|
|
105
|
+
createPixel(pixelColor) {
|
|
106
|
+
const { gridSizeX, gridSizeY, pixelSizeX, pixelSizeY, pixelGap } = this;
|
|
107
|
+
let realGridSizeX = gridSizeX;
|
|
108
|
+
let realGridSizeY = gridSizeY;
|
|
109
|
+
if (this.mode !== 'grid') {
|
|
110
|
+
realGridSizeX = (this.width - pixelGap) / (pixelSizeX + pixelGap);
|
|
111
|
+
realGridSizeY = (this.height - pixelGap) / (pixelSizeY + pixelGap);
|
|
112
|
+
}
|
|
113
|
+
for (let x = 0; x < realGridSizeX; x++) {
|
|
114
|
+
for (let y = 0; y < realGridSizeY; y++) {
|
|
115
|
+
this.fillPixel(x, y, pixelColor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
fillPixel(x, y, color) {
|
|
120
|
+
const { ctx, pixelSizeX, pixelSizeY, pixelGap, pixelShape } = this;
|
|
121
|
+
const offsetX = pixelGap + x * (pixelSizeX + pixelGap);
|
|
122
|
+
const offsetY = pixelGap + y * (pixelSizeY + pixelGap);
|
|
123
|
+
// 清除原有填充颜色
|
|
124
|
+
ctx.clearRect(offsetX, offsetY, pixelSizeX, pixelSizeY);
|
|
125
|
+
ctx.fillStyle = color; // 填充颜色
|
|
126
|
+
if (pixelShape === 'square') {
|
|
127
|
+
ctx.fillRect(offsetX, offsetY, pixelSizeX, pixelSizeY);
|
|
128
|
+
} else {
|
|
129
|
+
const radiusX = pixelSizeX / 2;
|
|
130
|
+
const radiusY = pixelSizeY / 2;
|
|
131
|
+
// 开始绘制路径
|
|
132
|
+
ctx.beginPath();
|
|
133
|
+
// 使用arc方法绘制圆形,传入圆心x坐标、圆心y坐标、半径、起始角度(弧度制)、结束角度(弧度制)
|
|
134
|
+
if (radiusX === radiusY) {
|
|
135
|
+
ctx.arc(offsetX + radiusX, offsetY + radiusY, radiusX, 0, Math.PI * 2);
|
|
136
|
+
} else {
|
|
137
|
+
ctx.ellipse(offsetX + radiusX, offsetY + radiusY, radiusX, radiusY, 0, 0, Math.PI * 2);
|
|
138
|
+
// ctx.arc(offsetX + radiusX, (offsetY + radiusY) / (radiusY / radiusX), radiusX, 0, Math.PI * 2);
|
|
139
|
+
}
|
|
140
|
+
// 关闭路径
|
|
141
|
+
ctx.closePath();
|
|
142
|
+
|
|
143
|
+
// 执行填充操作,将圆形内部填充为设定的颜色
|
|
144
|
+
ctx.fill();
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
// 改变画笔颜色
|
|
148
|
+
updateColor(color) {
|
|
149
|
+
this.penColor = color;
|
|
150
|
+
},
|
|
151
|
+
// 油漆桶
|
|
152
|
+
changeBg(color) {
|
|
153
|
+
this.penColor = color;
|
|
154
|
+
this.createPixel(color);
|
|
155
|
+
},
|
|
156
|
+
// 清除画布
|
|
157
|
+
clear() {
|
|
158
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
159
|
+
this.createPixel(this.pixelColor);
|
|
160
|
+
},
|
|
161
|
+
save() {
|
|
162
|
+
const base64 = this.canvas.toDataURL('image/png');
|
|
163
|
+
this.callMethod('genImageData', { base64 });
|
|
164
|
+
},
|
|
165
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const tuya = {
|
|
2
|
+
backgroundColor: '#f2f4f6',
|
|
3
|
+
navigationBarTitleText: 'Ray Graffiti'
|
|
4
|
+
};
|
|
5
|
+
export const wechat = {
|
|
6
|
+
backgroundColor: '#f2f4f6',
|
|
7
|
+
navigationBarTitleText: 'Ray Graffiti'
|
|
8
|
+
};
|
|
9
|
+
export const native = {
|
|
10
|
+
backgroundColor: 'transparent',
|
|
11
|
+
isBleOfflineOverlay: false,
|
|
12
|
+
useSafeAreaView: true,
|
|
13
|
+
navigationBarTitleText: 'Ray Graffiti'
|
|
14
|
+
};
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
|
+
import { View } from '@ray-js/ray';
|
|
4
|
+
import Graffiti from './components';
|
|
5
|
+
import { getUuid } from './utils';
|
|
6
|
+
import { defaultProps } from './props';
|
|
7
|
+
const classPrefix = 'ray-graffiti';
|
|
8
|
+
const RayGraffiti = props => {
|
|
9
|
+
const canvasIdPrefix = useMemo(() => {
|
|
10
|
+
return `${getUuid(classPrefix)}`;
|
|
11
|
+
}, []);
|
|
12
|
+
const {
|
|
13
|
+
style = {},
|
|
14
|
+
className = '',
|
|
15
|
+
width,
|
|
16
|
+
height,
|
|
17
|
+
mode,
|
|
18
|
+
gridSizeX,
|
|
19
|
+
gridSizeY,
|
|
20
|
+
pixelSizeX,
|
|
21
|
+
pixelSizeY,
|
|
22
|
+
pixelGap,
|
|
23
|
+
pixelShape,
|
|
24
|
+
pixelColor,
|
|
25
|
+
penColor,
|
|
26
|
+
actionType,
|
|
27
|
+
needStroke,
|
|
28
|
+
saveTrigger,
|
|
29
|
+
clearTrigger,
|
|
30
|
+
onStrokeChange,
|
|
31
|
+
onSaveData
|
|
32
|
+
} = props;
|
|
33
|
+
return /*#__PURE__*/React.createElement(View, {
|
|
34
|
+
className: `${classPrefix} ${className}`,
|
|
35
|
+
style: _objectSpread({}, style)
|
|
36
|
+
}, /*#__PURE__*/React.createElement(Graffiti, {
|
|
37
|
+
canvasIdPrefix: canvasIdPrefix,
|
|
38
|
+
width: width,
|
|
39
|
+
height: height,
|
|
40
|
+
mode: mode,
|
|
41
|
+
gridSizeX: gridSizeX,
|
|
42
|
+
gridSizeY: gridSizeY,
|
|
43
|
+
pixelSizeX: pixelSizeX,
|
|
44
|
+
pixelSizeY: pixelSizeY,
|
|
45
|
+
pixelGap: pixelGap,
|
|
46
|
+
pixelShape: pixelShape,
|
|
47
|
+
pixelColor: pixelColor,
|
|
48
|
+
penColor: penColor,
|
|
49
|
+
actionType: actionType,
|
|
50
|
+
needStroke: needStroke,
|
|
51
|
+
saveTrigger: saveTrigger,
|
|
52
|
+
clearTrigger: clearTrigger,
|
|
53
|
+
bindstrokeChange: e => onStrokeChange(e.detail),
|
|
54
|
+
bindsaveData: e => onSaveData(e.detail)
|
|
55
|
+
}));
|
|
56
|
+
};
|
|
57
|
+
RayGraffiti.defaultProps = defaultProps;
|
|
58
|
+
RayGraffiti.displayName = classPrefix;
|
|
59
|
+
export default RayGraffiti;
|
package/lib/props.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
export interface IProps {
|
|
3
|
+
/**
|
|
4
|
+
* @description.zh 样式
|
|
5
|
+
* @description.en Style
|
|
6
|
+
* @default {}
|
|
7
|
+
*/
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
/**
|
|
10
|
+
* @description.zh 类名
|
|
11
|
+
* @description.en className
|
|
12
|
+
* @default ''
|
|
13
|
+
*/
|
|
14
|
+
className?: string;
|
|
15
|
+
/**
|
|
16
|
+
* @description.zh 画布宽度
|
|
17
|
+
* @description.en Canvas width
|
|
18
|
+
* @default 353
|
|
19
|
+
*/
|
|
20
|
+
width?: number;
|
|
21
|
+
/**
|
|
22
|
+
* @description.zh 画布高度
|
|
23
|
+
* @description.en Canvas height
|
|
24
|
+
* @default 353
|
|
25
|
+
*/
|
|
26
|
+
height?: number;
|
|
27
|
+
/**
|
|
28
|
+
* @description.zh 画布显示模式(grid: 按格子数量显示, pixel: 按像素显示格子), 如果 mode 设置为 grid, 那么 pixelSizeX, pixelSizeY 属性将不生效, 如果 mode 设置为 pixel, 那么 gridSizeX, gridSizeY 属性将不生效, 推荐使用 grid 模式
|
|
29
|
+
* @description.en Canvas display mode (grid: display by grid number, pixel: display by pixel grid), if mode is set to grid, then pixelSizeX, pixelSizeY properties will not take effect, if mode is set to pixel, then gridSizeX, gridSizeY properties will not take effect, it is recommended to use grid mode
|
|
30
|
+
* @default grid
|
|
31
|
+
*/
|
|
32
|
+
mode?: 'grid' | 'pixel';
|
|
33
|
+
/**
|
|
34
|
+
* @description.zh 每行格子数量
|
|
35
|
+
* @description.en Number of grid per row and column
|
|
36
|
+
* @default 32
|
|
37
|
+
*/
|
|
38
|
+
gridSizeX?: number;
|
|
39
|
+
/**
|
|
40
|
+
* @description.zh 每列格子数量
|
|
41
|
+
* @description.en Number of grid per column
|
|
42
|
+
* @default 32
|
|
43
|
+
*/
|
|
44
|
+
gridSizeY?: number;
|
|
45
|
+
/**
|
|
46
|
+
* @description.zh 格子宽度
|
|
47
|
+
* @description.en Pixel width
|
|
48
|
+
* @default 10
|
|
49
|
+
*/
|
|
50
|
+
pixelSizeX?: number;
|
|
51
|
+
/**
|
|
52
|
+
* @description.zh 格子高度
|
|
53
|
+
* @description.en Pixel height
|
|
54
|
+
* @default 10
|
|
55
|
+
*/
|
|
56
|
+
pixelSizeY?: number;
|
|
57
|
+
/**
|
|
58
|
+
* @description.zh 格子间距
|
|
59
|
+
* @description.en Pixel gap
|
|
60
|
+
* @default 1
|
|
61
|
+
*/
|
|
62
|
+
pixelGap?: number;
|
|
63
|
+
/**
|
|
64
|
+
* @description.zh 格子形状, square 方形, circle 圆形
|
|
65
|
+
* @description.en Pixel shape
|
|
66
|
+
* @default square
|
|
67
|
+
*/
|
|
68
|
+
pixelShape?: 'square' | 'circle';
|
|
69
|
+
/**
|
|
70
|
+
* @description.zh 网格颜色
|
|
71
|
+
* @description.en Grid color
|
|
72
|
+
* @default `rgba(255, 255, 255, 0.15)`
|
|
73
|
+
*/
|
|
74
|
+
pixelColor?: string;
|
|
75
|
+
/**
|
|
76
|
+
* @description.zh 画笔颜色
|
|
77
|
+
* @description.en Pen color
|
|
78
|
+
* @default `rgb(255, 255, 255)`
|
|
79
|
+
*/
|
|
80
|
+
penColor?: string;
|
|
81
|
+
/**
|
|
82
|
+
* @description.zh 操作类型, pencil 画笔, eraser 橡皮擦, paint 油漆桶
|
|
83
|
+
* @description.en Action type
|
|
84
|
+
* @default pencil
|
|
85
|
+
*/
|
|
86
|
+
actionType?: 'pencil' | 'eraser' | 'paint';
|
|
87
|
+
/**
|
|
88
|
+
* @description.zh 是否监听每一画笔数据, 开启后可以通过 onStrokeChange 获取每一笔画笔数据
|
|
89
|
+
* @description.en Whether to listen to each stroke data, after opening, you can get each stroke data through onStrokeChange
|
|
90
|
+
* @default false
|
|
91
|
+
*/
|
|
92
|
+
needStroke?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* @description.zh 触发保存的标识, 每次递增该值, 可以触发 onSaveData 返回保存数据
|
|
95
|
+
* @description.en Trigger save mark, increase this value each time, you can trigger onSaveData to return saved data
|
|
96
|
+
* @default 0
|
|
97
|
+
*/
|
|
98
|
+
saveTrigger?: number;
|
|
99
|
+
/**
|
|
100
|
+
* @description.zh 触发清空的标识, 每次递增该值, 可以触发画笔清空
|
|
101
|
+
* @description.en Trigger clear mark, increase this value each time, you can trigger the pen to clear
|
|
102
|
+
* @default 0
|
|
103
|
+
*/
|
|
104
|
+
clearTrigger?: number;
|
|
105
|
+
/**
|
|
106
|
+
* @description.zh 画笔数据更新时触发, 返回画笔经过的x,y坐标路径数据
|
|
107
|
+
* @description.en Triggered when the brush data is updated, returns the x,y coordinate path data of the brush.
|
|
108
|
+
* @default () => null
|
|
109
|
+
*/
|
|
110
|
+
onStrokeChange?: (data: IStrokeData) => void;
|
|
111
|
+
/**
|
|
112
|
+
* @description.zh 保存数据时触发, 返回画布的base64数据
|
|
113
|
+
* @description.en Triggered when saving data, return canvas base64 data
|
|
114
|
+
* @default () => null
|
|
115
|
+
*/
|
|
116
|
+
onSaveData?: (data: IData) => void;
|
|
117
|
+
}
|
|
118
|
+
export type IStrokeData = {
|
|
119
|
+
points: Array<{
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
}>;
|
|
123
|
+
};
|
|
124
|
+
export type IData = {
|
|
125
|
+
base64: string;
|
|
126
|
+
};
|
|
127
|
+
export declare const defaultProps: IProps;
|
package/lib/props.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const defaultProps = {
|
|
2
|
+
style: {},
|
|
3
|
+
width: 353,
|
|
4
|
+
height: 353,
|
|
5
|
+
mode: 'grid',
|
|
6
|
+
gridSizeX: 32,
|
|
7
|
+
gridSizeY: 32,
|
|
8
|
+
pixelSizeX: 10,
|
|
9
|
+
pixelSizeY: 10,
|
|
10
|
+
pixelGap: 1,
|
|
11
|
+
pixelShape: 'square',
|
|
12
|
+
pixelColor: 'rgba(255, 255, 255, 0.15)',
|
|
13
|
+
penColor: 'rgb(255, 255, 255)',
|
|
14
|
+
actionType: 'pencil',
|
|
15
|
+
needStroke: false,
|
|
16
|
+
saveTrigger: 0,
|
|
17
|
+
clearTrigger: 0,
|
|
18
|
+
onStrokeChange: () => null,
|
|
19
|
+
onSaveData: () => null
|
|
20
|
+
};
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getUuid: (prefix?: string) => string;
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const getUuid = function () {
|
|
2
|
+
let prefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
|
|
3
|
+
const id = `${String(+new Date()).slice(-3)}_${String(Math.random()).slice(-3)}`;
|
|
4
|
+
if (prefix) {
|
|
5
|
+
return `${prefix}_${id}`;
|
|
6
|
+
}
|
|
7
|
+
return id;
|
|
8
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ray-js/graffiti",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "画布涂鸦组件",
|
|
5
|
+
"main": "lib/index",
|
|
6
|
+
"files": [
|
|
7
|
+
"lib"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"types": "lib/index.d.ts",
|
|
11
|
+
"maintainers": [
|
|
12
|
+
"tuya_npm",
|
|
13
|
+
{
|
|
14
|
+
"name": "tuyafe",
|
|
15
|
+
"email": "tuyafe@tuya.com"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"lint": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
|
|
20
|
+
"lint:style": "stylelint \"src/**/*.less\" --fix",
|
|
21
|
+
"build": "ray build --type=component",
|
|
22
|
+
"watch": "ray start --type=component --output ./example/src/lib",
|
|
23
|
+
"build:tuya": "ray build -t tuya ./example",
|
|
24
|
+
"build:wechat": "ray build ./example --target=wechat",
|
|
25
|
+
"build:web": "ray build ./example --target=web",
|
|
26
|
+
"build:native": "ray build ./example --target=native",
|
|
27
|
+
"start:native": "ray start ./example -t native --verbose",
|
|
28
|
+
"start:tuya": "ray start -t tuya ./example",
|
|
29
|
+
"start:wechat": "ray start ./example -t wechat --verbose",
|
|
30
|
+
"start:web": "ray start ./example -t web",
|
|
31
|
+
"prepublishOnly": "yarn build",
|
|
32
|
+
"release-it": "standard-version"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@ray-js/ray": "^1.4.9"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"clsx": "^1.2.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@commitlint/cli": "^7.2.1",
|
|
42
|
+
"@commitlint/config-conventional": "^9.0.1",
|
|
43
|
+
"@ray-js/cli": "^1.4.9",
|
|
44
|
+
"@ray-js/components-ty-lamp": "^2.0.2",
|
|
45
|
+
"@ray-js/lamp-saturation-slider": "^1.1.7",
|
|
46
|
+
"@ray-js/panel-sdk": "^1.13.0-storage.7",
|
|
47
|
+
"@ray-js/ray": "^1.4.9",
|
|
48
|
+
"@vant/stylelint-config": "^1.4.2",
|
|
49
|
+
"core-js": "^3.19.1",
|
|
50
|
+
"eslint-config-tuya-panel": "^0.4.2",
|
|
51
|
+
"husky": "^1.2.0",
|
|
52
|
+
"lint-staged": "^10.2.11",
|
|
53
|
+
"standard-version": "9.3.2",
|
|
54
|
+
"stylelint": "^13.0.0"
|
|
55
|
+
},
|
|
56
|
+
"resolutions": {
|
|
57
|
+
"@ray-js/builder-mp": "1.4.15"
|
|
58
|
+
},
|
|
59
|
+
"husky": {
|
|
60
|
+
"hooks": {
|
|
61
|
+
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS --config commitlint.config.js",
|
|
62
|
+
"pre-commit": "lint-staged"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"lint-staged": {
|
|
66
|
+
"*.{ts,tsx,js,jsx}": [
|
|
67
|
+
"eslint --fix",
|
|
68
|
+
"git add"
|
|
69
|
+
],
|
|
70
|
+
"*.{json,md,yml,yaml}": [
|
|
71
|
+
"prettier --write",
|
|
72
|
+
"git add"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|