@elizaos/capacitor-canvas 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/ElizaosCapacitorCanvas.podspec +18 -0
- package/android/build.gradle +48 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/ai/eliza/plugins/canvas/CanvasPlugin.kt +2175 -0
- package/dist/esm/definitions.d.ts +473 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/web.d.ts +154 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +857 -0
- package/dist/plugin.cjs.js +873 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +876 -0
- package/dist/plugin.js.map +1 -0
- package/electrobun/src/index.ts +872 -0
- package/electrobun/tsconfig.json +18 -0
- package/ios/Sources/CanvasPlugin/CanvasPlugin.swift +1933 -0
- package/package.json +81 -0
|
@@ -0,0 +1,2175 @@
|
|
|
1
|
+
package ai.eliza.plugins.canvas
|
|
2
|
+
|
|
3
|
+
import android.graphics.*
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import android.util.Base64
|
|
7
|
+
import android.view.MotionEvent
|
|
8
|
+
import android.view.View
|
|
9
|
+
import android.view.ViewGroup
|
|
10
|
+
import android.webkit.JavascriptInterface
|
|
11
|
+
import android.webkit.WebChromeClient
|
|
12
|
+
import android.webkit.WebResourceError
|
|
13
|
+
import android.webkit.WebResourceRequest
|
|
14
|
+
import android.webkit.WebSettings
|
|
15
|
+
import android.webkit.WebView
|
|
16
|
+
import android.webkit.WebViewClient
|
|
17
|
+
import android.widget.FrameLayout
|
|
18
|
+
import com.getcapacitor.JSArray
|
|
19
|
+
import com.getcapacitor.JSObject
|
|
20
|
+
import com.getcapacitor.Plugin
|
|
21
|
+
import com.getcapacitor.PluginCall
|
|
22
|
+
import com.getcapacitor.PluginMethod
|
|
23
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
24
|
+
import org.json.JSONArray
|
|
25
|
+
import org.json.JSONObject
|
|
26
|
+
import java.io.ByteArrayOutputStream
|
|
27
|
+
import java.net.URL
|
|
28
|
+
import java.util.UUID
|
|
29
|
+
import kotlin.math.tan
|
|
30
|
+
|
|
31
|
+
@CapacitorPlugin(name = "ElizaCanvas")
|
|
32
|
+
class CanvasPlugin : Plugin() {
|
|
33
|
+
|
|
34
|
+
private val canvases = mutableMapOf<String, ManagedCanvas>()
|
|
35
|
+
private var nextCanvasId = 1
|
|
36
|
+
private var nextLayerId = 1
|
|
37
|
+
|
|
38
|
+
// ---- Data Structures ----
|
|
39
|
+
|
|
40
|
+
data class CanvasSize(val width: Int, val height: Int)
|
|
41
|
+
|
|
42
|
+
class ManagedCanvas(
|
|
43
|
+
val id: String,
|
|
44
|
+
var view: CanvasView,
|
|
45
|
+
var webView: WebView? = null,
|
|
46
|
+
var layers: MutableMap<String, ManagedLayer> = mutableMapOf(),
|
|
47
|
+
var size: CanvasSize,
|
|
48
|
+
var touchEnabled: Boolean = false,
|
|
49
|
+
var globalTransform: Matrix = Matrix()
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
data class ManagedLayer(
|
|
53
|
+
val id: String,
|
|
54
|
+
var name: String?,
|
|
55
|
+
var visible: Boolean,
|
|
56
|
+
var opacity: Float,
|
|
57
|
+
var zIndex: Int,
|
|
58
|
+
var view: CanvasView
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// ---- CanvasView: a View backed by a Bitmap/Canvas ----
|
|
62
|
+
|
|
63
|
+
class CanvasView(context: android.content.Context, private var size: CanvasSize) :
|
|
64
|
+
View(context) {
|
|
65
|
+
|
|
66
|
+
private var bitmap: Bitmap =
|
|
67
|
+
Bitmap.createBitmap(size.width, size.height, Bitmap.Config.ARGB_8888)
|
|
68
|
+
private var drawCanvas: Canvas = Canvas(bitmap)
|
|
69
|
+
private val drawPaint = Paint()
|
|
70
|
+
var touchHandler: ((String, List<TouchInfo>) -> Unit)? = null
|
|
71
|
+
|
|
72
|
+
data class TouchInfo(
|
|
73
|
+
val id: Int, val x: Float, val y: Float, val pressure: Float?
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
override fun onDraw(canvas: Canvas) {
|
|
77
|
+
super.onDraw(canvas)
|
|
78
|
+
canvas.drawBitmap(bitmap, 0f, 0f, null)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun getDrawCanvas(): Canvas = drawCanvas
|
|
82
|
+
fun getBitmap(): Bitmap = bitmap
|
|
83
|
+
|
|
84
|
+
fun clear(rect: RectF? = null) {
|
|
85
|
+
if (rect != null) {
|
|
86
|
+
drawPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
|
87
|
+
drawCanvas.drawRect(rect, drawPaint)
|
|
88
|
+
drawPaint.xfermode = null
|
|
89
|
+
} else {
|
|
90
|
+
bitmap.eraseColor(Color.TRANSPARENT)
|
|
91
|
+
}
|
|
92
|
+
invalidate()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fun resize(newSize: CanvasSize) {
|
|
96
|
+
val newBitmap =
|
|
97
|
+
Bitmap.createBitmap(newSize.width, newSize.height, Bitmap.Config.ARGB_8888)
|
|
98
|
+
val newCanvas = Canvas(newBitmap)
|
|
99
|
+
newCanvas.drawBitmap(bitmap, 0f, 0f, null)
|
|
100
|
+
bitmap.recycle()
|
|
101
|
+
bitmap = newBitmap
|
|
102
|
+
drawCanvas = newCanvas
|
|
103
|
+
size = newSize
|
|
104
|
+
invalidate()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fun setImage(bmp: Bitmap?) {
|
|
108
|
+
if (bmp == null) {
|
|
109
|
+
bitmap.eraseColor(Color.TRANSPARENT)
|
|
110
|
+
} else {
|
|
111
|
+
drawCanvas.drawBitmap(bmp, 0f, 0f, null)
|
|
112
|
+
}
|
|
113
|
+
invalidate()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
117
|
+
val type = when (event.actionMasked) {
|
|
118
|
+
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> "start"
|
|
119
|
+
MotionEvent.ACTION_MOVE -> "move"
|
|
120
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> "end"
|
|
121
|
+
MotionEvent.ACTION_CANCEL -> "cancel"
|
|
122
|
+
else -> return super.onTouchEvent(event)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
val touches = mutableListOf<TouchInfo>()
|
|
126
|
+
for (i in 0 until event.pointerCount) {
|
|
127
|
+
touches.add(
|
|
128
|
+
TouchInfo(
|
|
129
|
+
id = event.getPointerId(i),
|
|
130
|
+
x = event.getX(i),
|
|
131
|
+
y = event.getY(i),
|
|
132
|
+
pressure = if (event.getPressure(i) > 0) event.getPressure(i) else null
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
touchHandler?.invoke(type, touches)
|
|
138
|
+
return true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fun commit() {
|
|
142
|
+
invalidate()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- Create / Destroy ----
|
|
147
|
+
|
|
148
|
+
@PluginMethod
|
|
149
|
+
fun create(call: PluginCall) {
|
|
150
|
+
val sizeObj = call.getObject("size") ?: run {
|
|
151
|
+
call.reject("Missing size parameter")
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
val width = sizeObj.int("width", 100)
|
|
156
|
+
val height = sizeObj.int("height", 100)
|
|
157
|
+
val size = CanvasSize(width, height)
|
|
158
|
+
|
|
159
|
+
val canvasId = "canvas_${nextCanvasId++}"
|
|
160
|
+
|
|
161
|
+
activity.runOnUiThread {
|
|
162
|
+
val view = CanvasView(context, size)
|
|
163
|
+
view.layoutParams = ViewGroup.LayoutParams(width, height)
|
|
164
|
+
|
|
165
|
+
val bgColorObj = call.getObject("backgroundColor")
|
|
166
|
+
if (bgColorObj != null) {
|
|
167
|
+
val color = colorFromObject(bgColorObj)
|
|
168
|
+
view.setBackgroundColor(color)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
val canvas = ManagedCanvas(canvasId, view, size = size)
|
|
172
|
+
canvases[canvasId] = canvas
|
|
173
|
+
|
|
174
|
+
call.resolve(JSObject().apply {
|
|
175
|
+
put("canvasId", canvasId)
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@PluginMethod
|
|
181
|
+
fun destroy(call: PluginCall) {
|
|
182
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
183
|
+
call.reject("Missing canvasId")
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
activity.runOnUiThread {
|
|
188
|
+
canvases[canvasId]?.let { canvas ->
|
|
189
|
+
canvas.webView?.let { wv ->
|
|
190
|
+
wv.destroy()
|
|
191
|
+
(wv.parent as? ViewGroup)?.removeView(wv)
|
|
192
|
+
}
|
|
193
|
+
(canvas.view.parent as? ViewGroup)?.removeView(canvas.view)
|
|
194
|
+
canvas.layers.values.forEach { layer ->
|
|
195
|
+
(layer.view.parent as? ViewGroup)?.removeView(layer.view)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
canvases.remove(canvasId)
|
|
199
|
+
call.resolve()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---- Attach / Detach ----
|
|
204
|
+
|
|
205
|
+
@PluginMethod
|
|
206
|
+
fun attach(call: PluginCall) {
|
|
207
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
208
|
+
call.reject("Missing canvasId")
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
val canvas = canvases[canvasId] ?: run {
|
|
213
|
+
call.reject("Canvas not found")
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
activity.runOnUiThread {
|
|
218
|
+
val webView = bridge.webView
|
|
219
|
+
val parent = webView?.parent as? ViewGroup
|
|
220
|
+
|
|
221
|
+
if (parent != null) {
|
|
222
|
+
canvas.view.layoutParams = FrameLayout.LayoutParams(
|
|
223
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
224
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
parent.addView(canvas.view, 0)
|
|
228
|
+
webView.setBackgroundColor(Color.TRANSPARENT)
|
|
229
|
+
|
|
230
|
+
// If a web canvas exists, ensure it's also in the hierarchy.
|
|
231
|
+
canvas.webView?.let { wv ->
|
|
232
|
+
if (wv.parent == null) {
|
|
233
|
+
wv.layoutParams = FrameLayout.LayoutParams(
|
|
234
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
235
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
236
|
+
)
|
|
237
|
+
parent.addView(wv, 0)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Set up touch handler.
|
|
242
|
+
canvas.view.touchHandler = { type, touches ->
|
|
243
|
+
if (canvas.touchEnabled) {
|
|
244
|
+
val touchArray = JSArray()
|
|
245
|
+
touches.forEach { touch ->
|
|
246
|
+
touchArray.put(JSObject().apply {
|
|
247
|
+
put("id", touch.id)
|
|
248
|
+
put("x", touch.x.toDouble())
|
|
249
|
+
put("y", touch.y.toDouble())
|
|
250
|
+
touch.pressure?.let { put("force", it.toDouble()) }
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
notifyListeners("touch", JSObject().apply {
|
|
255
|
+
put("type", type)
|
|
256
|
+
put("touches", touchArray)
|
|
257
|
+
put("timestamp", System.currentTimeMillis())
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
call.resolve()
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@PluginMethod
|
|
268
|
+
fun detach(call: PluginCall) {
|
|
269
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
270
|
+
call.reject("Missing canvasId")
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
val canvas = canvases[canvasId] ?: run {
|
|
275
|
+
call.reject("Canvas not found")
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
activity.runOnUiThread {
|
|
280
|
+
(canvas.view.parent as? ViewGroup)?.removeView(canvas.view)
|
|
281
|
+
call.resolve()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---- Resize / Clear ----
|
|
286
|
+
|
|
287
|
+
@PluginMethod
|
|
288
|
+
fun resize(call: PluginCall) {
|
|
289
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
290
|
+
call.reject("Missing canvasId")
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
val sizeObj = call.getObject("size") ?: run {
|
|
295
|
+
call.reject("Missing size")
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
val canvas = canvases[canvasId] ?: run {
|
|
300
|
+
call.reject("Canvas not found")
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
val width = sizeObj.int("width", canvas.size.width)
|
|
305
|
+
val height = sizeObj.int("height", canvas.size.height)
|
|
306
|
+
val newSize = CanvasSize(width, height)
|
|
307
|
+
|
|
308
|
+
activity.runOnUiThread {
|
|
309
|
+
canvas.size = newSize
|
|
310
|
+
canvas.view.resize(newSize)
|
|
311
|
+
canvas.webView?.layoutParams =
|
|
312
|
+
FrameLayout.LayoutParams(width, height)
|
|
313
|
+
canvas.layers.values.forEach { it.view.resize(newSize) }
|
|
314
|
+
call.resolve()
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@PluginMethod
|
|
319
|
+
fun clear(call: PluginCall) {
|
|
320
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
321
|
+
call.reject("Missing canvasId")
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
val canvas = canvases[canvasId] ?: run {
|
|
326
|
+
call.reject("Canvas not found")
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
val layerId = call.getString("layerId")
|
|
331
|
+
val rectObj = call.getObject("rect")
|
|
332
|
+
|
|
333
|
+
activity.runOnUiThread {
|
|
334
|
+
val targetView = layerId?.let { canvas.layers[it]?.view } ?: canvas.view
|
|
335
|
+
|
|
336
|
+
val rect = rectObj?.let {
|
|
337
|
+
RectF(
|
|
338
|
+
it.float("x"),
|
|
339
|
+
it.float("y"),
|
|
340
|
+
it.float("x") + it.float("width"),
|
|
341
|
+
it.float("y") + it.float("height")
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
targetView.clear(rect)
|
|
346
|
+
call.resolve()
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---- Layer Operations ----
|
|
351
|
+
|
|
352
|
+
@PluginMethod
|
|
353
|
+
fun createLayer(call: PluginCall) {
|
|
354
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
355
|
+
call.reject("Missing canvasId")
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
val canvas = canvases[canvasId] ?: run {
|
|
360
|
+
call.reject("Canvas not found")
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
val layerObj = call.getObject("layer") ?: run {
|
|
365
|
+
call.reject("Missing layer")
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
val layerId = "layer_${nextLayerId++}"
|
|
370
|
+
val visible = layerObj.boolean("visible", true)
|
|
371
|
+
val opacity = layerObj.float("opacity", 1f)
|
|
372
|
+
val zIndex = layerObj.int("zIndex")
|
|
373
|
+
val name = layerObj.getString("name")
|
|
374
|
+
|
|
375
|
+
activity.runOnUiThread {
|
|
376
|
+
val view = CanvasView(context, canvas.size)
|
|
377
|
+
view.layoutParams = FrameLayout.LayoutParams(
|
|
378
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
379
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
380
|
+
)
|
|
381
|
+
view.alpha = opacity
|
|
382
|
+
view.visibility = if (visible) View.VISIBLE else View.GONE
|
|
383
|
+
|
|
384
|
+
val layer = ManagedLayer(layerId, name, visible, opacity, zIndex, view)
|
|
385
|
+
canvas.layers[layerId] = layer
|
|
386
|
+
|
|
387
|
+
val parent = canvas.view.parent as? ViewGroup
|
|
388
|
+
parent?.addView(view)
|
|
389
|
+
sortLayers(canvas)
|
|
390
|
+
|
|
391
|
+
call.resolve(JSObject().apply {
|
|
392
|
+
put("layerId", layerId)
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@PluginMethod
|
|
398
|
+
fun updateLayer(call: PluginCall) {
|
|
399
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
400
|
+
call.reject("Missing canvasId")
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
val layerId = call.getString("layerId") ?: run {
|
|
404
|
+
call.reject("Missing layerId")
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
val canvas = canvases[canvasId] ?: run {
|
|
408
|
+
call.reject("Canvas not found")
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
val layer = canvas.layers[layerId] ?: run {
|
|
412
|
+
call.reject("Layer not found")
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
val layerObj = call.getObject("layer") ?: run {
|
|
416
|
+
call.reject("Missing layer")
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
activity.runOnUiThread {
|
|
421
|
+
layerObj.booleanOrNull("visible")?.let {
|
|
422
|
+
layer.visible = it
|
|
423
|
+
layer.view.visibility = if (it) View.VISIBLE else View.GONE
|
|
424
|
+
}
|
|
425
|
+
layerObj.floatOrNull("opacity")?.let {
|
|
426
|
+
layer.opacity = it
|
|
427
|
+
layer.view.alpha = layer.opacity
|
|
428
|
+
}
|
|
429
|
+
layerObj.intOrNull("zIndex")?.let {
|
|
430
|
+
layer.zIndex = it
|
|
431
|
+
sortLayers(canvas)
|
|
432
|
+
}
|
|
433
|
+
layerObj.getString("name")?.let {
|
|
434
|
+
layer.name = it
|
|
435
|
+
}
|
|
436
|
+
call.resolve()
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
@PluginMethod
|
|
441
|
+
fun deleteLayer(call: PluginCall) {
|
|
442
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
443
|
+
call.reject("Missing canvasId")
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
val layerId = call.getString("layerId") ?: run {
|
|
447
|
+
call.reject("Missing layerId")
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
val canvas = canvases[canvasId] ?: run {
|
|
451
|
+
call.reject("Canvas not found")
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
val layer = canvas.layers[layerId] ?: run {
|
|
455
|
+
call.reject("Layer not found")
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
activity.runOnUiThread {
|
|
460
|
+
(layer.view.parent as? ViewGroup)?.removeView(layer.view)
|
|
461
|
+
canvas.layers.remove(layerId)
|
|
462
|
+
call.resolve()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
@PluginMethod
|
|
467
|
+
fun getLayers(call: PluginCall) {
|
|
468
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
469
|
+
call.reject("Missing canvasId")
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
val canvas = canvases[canvasId] ?: run {
|
|
473
|
+
call.reject("Canvas not found")
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
val layers = JSArray()
|
|
478
|
+
canvas.layers.values.forEach { layer ->
|
|
479
|
+
layers.put(JSObject().apply {
|
|
480
|
+
put("id", layer.id)
|
|
481
|
+
put("visible", layer.visible)
|
|
482
|
+
put("opacity", layer.opacity.toDouble())
|
|
483
|
+
put("zIndex", layer.zIndex)
|
|
484
|
+
layer.name?.let { put("name", it) }
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
call.resolve(JSObject().apply {
|
|
489
|
+
put("layers", layers)
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ---- Drawing: Rect ----
|
|
494
|
+
|
|
495
|
+
@PluginMethod
|
|
496
|
+
fun drawRect(call: PluginCall) {
|
|
497
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
498
|
+
call.reject("Missing canvasId")
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
val canvas = canvases[canvasId] ?: run {
|
|
502
|
+
call.reject("Canvas not found")
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
val rectObj = call.getObject("rect") ?: run {
|
|
506
|
+
call.reject("Missing rect")
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
val drawOpts = call.getObject("drawOptions")
|
|
511
|
+
val layerId = drawOpts?.getString("layerId")
|
|
512
|
+
val fillObj = call.getObject("fill")
|
|
513
|
+
val strokeObj = call.getObject("stroke")
|
|
514
|
+
val cornerRadius = call.getFloat("cornerRadius") ?: 0f
|
|
515
|
+
|
|
516
|
+
activity.runOnUiThread {
|
|
517
|
+
val targetView = layerId?.let { canvas.layers[it]?.view } ?: canvas.view
|
|
518
|
+
val drawCanvas = targetView.getDrawCanvas()
|
|
519
|
+
val paint = Paint().apply { isAntiAlias = true }
|
|
520
|
+
val saveCount = applyDrawOptions(drawCanvas, canvas, drawOpts)
|
|
521
|
+
|
|
522
|
+
val rect = rectFromObject(rectObj)
|
|
523
|
+
|
|
524
|
+
fillObj?.let {
|
|
525
|
+
val gradient = extractGradient(it)
|
|
526
|
+
if (gradient != null) {
|
|
527
|
+
paint.shader = createShader(gradient)
|
|
528
|
+
paint.style = Paint.Style.FILL
|
|
529
|
+
if (cornerRadius > 0) {
|
|
530
|
+
drawCanvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)
|
|
531
|
+
} else {
|
|
532
|
+
drawCanvas.drawRect(rect, paint)
|
|
533
|
+
}
|
|
534
|
+
paint.shader = null
|
|
535
|
+
} else {
|
|
536
|
+
paint.color = colorFromFillOrStroke(it)
|
|
537
|
+
paint.style = Paint.Style.FILL
|
|
538
|
+
if (cornerRadius > 0) {
|
|
539
|
+
drawCanvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)
|
|
540
|
+
} else {
|
|
541
|
+
drawCanvas.drawRect(rect, paint)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
strokeObj?.let {
|
|
547
|
+
paint.color = colorFromFillOrStroke(it)
|
|
548
|
+
paint.style = Paint.Style.STROKE
|
|
549
|
+
paint.strokeWidth = it.float("width", 1f)
|
|
550
|
+
applyStrokeStyle(paint, it)
|
|
551
|
+
if (cornerRadius > 0) {
|
|
552
|
+
drawCanvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)
|
|
553
|
+
} else {
|
|
554
|
+
drawCanvas.drawRect(rect, paint)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
restoreDrawOptions(drawCanvas, saveCount)
|
|
559
|
+
targetView.commit()
|
|
560
|
+
call.resolve()
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ---- Drawing: Ellipse ----
|
|
565
|
+
|
|
566
|
+
@PluginMethod
|
|
567
|
+
fun drawEllipse(call: PluginCall) {
|
|
568
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
569
|
+
call.reject("Missing canvasId")
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
val canvas = canvases[canvasId] ?: run {
|
|
573
|
+
call.reject("Canvas not found")
|
|
574
|
+
return
|
|
575
|
+
}
|
|
576
|
+
val centerObj = call.getObject("center") ?: run {
|
|
577
|
+
call.reject("Missing center")
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
val radiusX = call.getFloat("radiusX") ?: 0f
|
|
582
|
+
val radiusY = call.getFloat("radiusY") ?: 0f
|
|
583
|
+
val drawOpts = call.getObject("drawOptions")
|
|
584
|
+
val layerId = drawOpts?.getString("layerId")
|
|
585
|
+
val fillObj = call.getObject("fill")
|
|
586
|
+
val strokeObj = call.getObject("stroke")
|
|
587
|
+
|
|
588
|
+
activity.runOnUiThread {
|
|
589
|
+
val targetView = layerId?.let { canvas.layers[it]?.view } ?: canvas.view
|
|
590
|
+
val drawCanvas = targetView.getDrawCanvas()
|
|
591
|
+
val paint = Paint().apply { isAntiAlias = true }
|
|
592
|
+
val saveCount = applyDrawOptions(drawCanvas, canvas, drawOpts)
|
|
593
|
+
|
|
594
|
+
val cx = centerObj.float("x")
|
|
595
|
+
val cy = centerObj.float("y")
|
|
596
|
+
val rect = RectF(cx - radiusX, cy - radiusY, cx + radiusX, cy + radiusY)
|
|
597
|
+
|
|
598
|
+
fillObj?.let {
|
|
599
|
+
val gradient = extractGradient(it)
|
|
600
|
+
if (gradient != null) {
|
|
601
|
+
paint.shader = createShader(gradient)
|
|
602
|
+
paint.style = Paint.Style.FILL
|
|
603
|
+
drawCanvas.drawOval(rect, paint)
|
|
604
|
+
paint.shader = null
|
|
605
|
+
} else {
|
|
606
|
+
paint.color = colorFromFillOrStroke(it)
|
|
607
|
+
paint.style = Paint.Style.FILL
|
|
608
|
+
drawCanvas.drawOval(rect, paint)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
strokeObj?.let {
|
|
613
|
+
paint.color = colorFromFillOrStroke(it)
|
|
614
|
+
paint.style = Paint.Style.STROKE
|
|
615
|
+
paint.strokeWidth = it.float("width", 1f)
|
|
616
|
+
applyStrokeStyle(paint, it)
|
|
617
|
+
drawCanvas.drawOval(rect, paint)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
restoreDrawOptions(drawCanvas, saveCount)
|
|
621
|
+
targetView.commit()
|
|
622
|
+
call.resolve()
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ---- Drawing: Line ----
|
|
627
|
+
|
|
628
|
+
@PluginMethod
|
|
629
|
+
fun drawLine(call: PluginCall) {
|
|
630
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
631
|
+
call.reject("Missing canvasId")
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
val canvas = canvases[canvasId] ?: run {
|
|
635
|
+
call.reject("Canvas not found")
|
|
636
|
+
return
|
|
637
|
+
}
|
|
638
|
+
val fromObj = call.getObject("from") ?: run {
|
|
639
|
+
call.reject("Missing from")
|
|
640
|
+
return
|
|
641
|
+
}
|
|
642
|
+
val toObj = call.getObject("to") ?: run {
|
|
643
|
+
call.reject("Missing to")
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
val strokeObj = call.getObject("stroke") ?: run {
|
|
647
|
+
call.reject("Missing stroke")
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
val drawOpts = call.getObject("drawOptions")
|
|
651
|
+
val layerId = drawOpts?.getString("layerId")
|
|
652
|
+
|
|
653
|
+
activity.runOnUiThread {
|
|
654
|
+
val targetView = layerId?.let { canvas.layers[it]?.view } ?: canvas.view
|
|
655
|
+
val drawCanvas = targetView.getDrawCanvas()
|
|
656
|
+
val saveCount = applyDrawOptions(drawCanvas, canvas, drawOpts)
|
|
657
|
+
|
|
658
|
+
val paint = Paint().apply {
|
|
659
|
+
isAntiAlias = true
|
|
660
|
+
style = Paint.Style.STROKE
|
|
661
|
+
color = colorFromFillOrStroke(strokeObj)
|
|
662
|
+
strokeWidth = strokeObj.float("width", 1f)
|
|
663
|
+
}
|
|
664
|
+
applyStrokeStyle(paint, strokeObj)
|
|
665
|
+
|
|
666
|
+
drawCanvas.drawLine(
|
|
667
|
+
fromObj.float("x"),
|
|
668
|
+
fromObj.float("y"),
|
|
669
|
+
toObj.float("x"),
|
|
670
|
+
toObj.float("y"),
|
|
671
|
+
paint
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
restoreDrawOptions(drawCanvas, saveCount)
|
|
675
|
+
targetView.commit()
|
|
676
|
+
call.resolve()
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ---- Drawing: Path ----
|
|
681
|
+
|
|
682
|
+
@PluginMethod
|
|
683
|
+
fun drawPath(call: PluginCall) {
|
|
684
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
685
|
+
call.reject("Missing canvasId")
|
|
686
|
+
return
|
|
687
|
+
}
|
|
688
|
+
val canvas = canvases[canvasId] ?: run {
|
|
689
|
+
call.reject("Canvas not found")
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
val pathObj = call.getObject("path") ?: run {
|
|
693
|
+
call.reject("Missing path")
|
|
694
|
+
return
|
|
695
|
+
}
|
|
696
|
+
val commands = pathObj.arrayOrNull("commands") ?: run {
|
|
697
|
+
call.reject("Missing commands in path")
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
val drawOpts = call.getObject("drawOptions")
|
|
702
|
+
val layerId = drawOpts?.getString("layerId")
|
|
703
|
+
val fillObj = call.getObject("fill")
|
|
704
|
+
val strokeObj = call.getObject("stroke")
|
|
705
|
+
|
|
706
|
+
activity.runOnUiThread {
|
|
707
|
+
val targetView = layerId?.let { canvas.layers[it]?.view } ?: canvas.view
|
|
708
|
+
val drawCanvas = targetView.getDrawCanvas()
|
|
709
|
+
val paint = Paint().apply { isAntiAlias = true }
|
|
710
|
+
val saveCount = applyDrawOptions(drawCanvas, canvas, drawOpts)
|
|
711
|
+
|
|
712
|
+
val path = buildPath(commands)
|
|
713
|
+
|
|
714
|
+
fillObj?.let {
|
|
715
|
+
val gradient = extractGradient(it)
|
|
716
|
+
if (gradient != null) {
|
|
717
|
+
paint.shader = createShader(gradient)
|
|
718
|
+
paint.style = Paint.Style.FILL
|
|
719
|
+
drawCanvas.drawPath(path, paint)
|
|
720
|
+
paint.shader = null
|
|
721
|
+
} else {
|
|
722
|
+
paint.color = colorFromFillOrStroke(it)
|
|
723
|
+
paint.style = Paint.Style.FILL
|
|
724
|
+
drawCanvas.drawPath(path, paint)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
strokeObj?.let {
|
|
729
|
+
paint.color = colorFromFillOrStroke(it)
|
|
730
|
+
paint.style = Paint.Style.STROKE
|
|
731
|
+
paint.strokeWidth = it.float("width", 1f)
|
|
732
|
+
applyStrokeStyle(paint, it)
|
|
733
|
+
drawCanvas.drawPath(path, paint)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
restoreDrawOptions(drawCanvas, saveCount)
|
|
737
|
+
targetView.commit()
|
|
738
|
+
call.resolve()
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Build an Android Path from the CanvasDrawPathCommand array. */
|
|
743
|
+
private fun buildPath(commands: JSONArray): Path {
|
|
744
|
+
val path = Path()
|
|
745
|
+
for (i in 0 until commands.length()) {
|
|
746
|
+
val cmd = commands.getJSONObject(i)
|
|
747
|
+
val type = cmd.optString("type", "")
|
|
748
|
+
val args = cmd.optJSONArray("args") ?: JSONArray()
|
|
749
|
+
val a = { idx: Int -> args.optDouble(idx, 0.0).toFloat() }
|
|
750
|
+
|
|
751
|
+
when (type) {
|
|
752
|
+
"moveTo" -> if (args.length() >= 2) {
|
|
753
|
+
path.moveTo(a(0), a(1))
|
|
754
|
+
}
|
|
755
|
+
"lineTo" -> if (args.length() >= 2) {
|
|
756
|
+
path.lineTo(a(0), a(1))
|
|
757
|
+
}
|
|
758
|
+
"quadraticCurveTo" -> if (args.length() >= 4) {
|
|
759
|
+
path.quadTo(a(0), a(1), a(2), a(3))
|
|
760
|
+
}
|
|
761
|
+
"bezierCurveTo" -> if (args.length() >= 6) {
|
|
762
|
+
path.cubicTo(a(0), a(1), a(2), a(3), a(4), a(5))
|
|
763
|
+
}
|
|
764
|
+
"arcTo" -> if (args.length() >= 5) {
|
|
765
|
+
// arcTo(x1, y1, x2, y2, radius) -- approximate with cubicTo.
|
|
766
|
+
// Android Path doesn't have tangent arc; use addArc as approximation.
|
|
767
|
+
val radius = a(4)
|
|
768
|
+
val oval = RectF(
|
|
769
|
+
a(0) - radius, a(1) - radius,
|
|
770
|
+
a(0) + radius, a(1) + radius
|
|
771
|
+
)
|
|
772
|
+
path.arcTo(oval, 0f, 90f)
|
|
773
|
+
}
|
|
774
|
+
"arc" -> if (args.length() >= 5) {
|
|
775
|
+
val cx = a(0)
|
|
776
|
+
val cy = a(1)
|
|
777
|
+
val radius = a(2)
|
|
778
|
+
val startAngle = Math.toDegrees(a(3).toDouble()).toFloat()
|
|
779
|
+
val endAngle = Math.toDegrees(a(4).toDouble()).toFloat()
|
|
780
|
+
val counterclockwise =
|
|
781
|
+
args.length() > 5 && args.optDouble(5, 0.0) != 0.0
|
|
782
|
+
val sweep = if (counterclockwise) {
|
|
783
|
+
-(((startAngle - endAngle) % 360 + 360) % 360)
|
|
784
|
+
} else {
|
|
785
|
+
(((endAngle - startAngle) % 360 + 360) % 360)
|
|
786
|
+
}
|
|
787
|
+
val oval = RectF(
|
|
788
|
+
cx - radius, cy - radius, cx + radius, cy + radius
|
|
789
|
+
)
|
|
790
|
+
path.arcTo(oval, startAngle, sweep)
|
|
791
|
+
}
|
|
792
|
+
"ellipse" -> if (args.length() >= 7) {
|
|
793
|
+
val cx = a(0)
|
|
794
|
+
val cy = a(1)
|
|
795
|
+
val rx = a(2)
|
|
796
|
+
val ry = a(3)
|
|
797
|
+
val rotation = a(4)
|
|
798
|
+
val startAngle = Math.toDegrees(a(5).toDouble()).toFloat()
|
|
799
|
+
val endAngle = Math.toDegrees(a(6).toDouble()).toFloat()
|
|
800
|
+
val counterclockwise =
|
|
801
|
+
args.length() > 7 && args.optDouble(7, 0.0) != 0.0
|
|
802
|
+
val sweep = if (counterclockwise) {
|
|
803
|
+
-(((startAngle - endAngle) % 360 + 360) % 360)
|
|
804
|
+
} else {
|
|
805
|
+
(((endAngle - startAngle) % 360 + 360) % 360)
|
|
806
|
+
}
|
|
807
|
+
val m = Matrix()
|
|
808
|
+
m.postTranslate(-cx, -cy)
|
|
809
|
+
m.postRotate(Math.toDegrees(rotation.toDouble()).toFloat())
|
|
810
|
+
m.postTranslate(cx, cy)
|
|
811
|
+
val subPath = Path()
|
|
812
|
+
val oval = RectF(cx - rx, cy - ry, cx + rx, cy + ry)
|
|
813
|
+
subPath.arcTo(oval, startAngle, sweep)
|
|
814
|
+
subPath.transform(m)
|
|
815
|
+
path.addPath(subPath)
|
|
816
|
+
}
|
|
817
|
+
"rect" -> if (args.length() >= 4) {
|
|
818
|
+
path.addRect(
|
|
819
|
+
a(0), a(1), a(0) + a(2), a(1) + a(3),
|
|
820
|
+
Path.Direction.CW
|
|
821
|
+
)
|
|
822
|
+
}
|
|
823
|
+
"closePath" -> path.close()
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return path
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ---- Drawing: Text ----
|
|
830
|
+
|
|
831
|
+
@PluginMethod
|
|
832
|
+
fun drawText(call: PluginCall) {
|
|
833
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
834
|
+
call.reject("Missing canvasId")
|
|
835
|
+
return
|
|
836
|
+
}
|
|
837
|
+
val canvas = canvases[canvasId] ?: run {
|
|
838
|
+
call.reject("Canvas not found")
|
|
839
|
+
return
|
|
840
|
+
}
|
|
841
|
+
val text = call.getString("text") ?: run {
|
|
842
|
+
call.reject("Missing text")
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
val positionObj = call.getObject("position") ?: run {
|
|
846
|
+
call.reject("Missing position")
|
|
847
|
+
return
|
|
848
|
+
}
|
|
849
|
+
val styleObj = call.getObject("style") ?: run {
|
|
850
|
+
call.reject("Missing style")
|
|
851
|
+
return
|
|
852
|
+
}
|
|
853
|
+
val drawOpts = call.getObject("drawOptions")
|
|
854
|
+
val layerId = drawOpts?.getString("layerId")
|
|
855
|
+
|
|
856
|
+
activity.runOnUiThread {
|
|
857
|
+
val targetView = layerId?.let { canvas.layers[it]?.view } ?: canvas.view
|
|
858
|
+
val drawCanvas = targetView.getDrawCanvas()
|
|
859
|
+
val saveCount = applyDrawOptions(drawCanvas, canvas, drawOpts)
|
|
860
|
+
|
|
861
|
+
val fontSize = styleObj.float("size", 14f)
|
|
862
|
+
val fontName = styleObj.getString("font") ?: "sans-serif"
|
|
863
|
+
val align = styleObj.getString("align") ?: "left"
|
|
864
|
+
val baseline = styleObj.getString("baseline") ?: "alphabetic"
|
|
865
|
+
val maxWidth = styleObj.floatOrNull("maxWidth")
|
|
866
|
+
|
|
867
|
+
val typeface = try {
|
|
868
|
+
Typeface.create(fontName, Typeface.NORMAL)
|
|
869
|
+
} catch (_: Exception) {
|
|
870
|
+
Typeface.DEFAULT
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
val paint = Paint().apply {
|
|
874
|
+
isAntiAlias = true
|
|
875
|
+
textSize = fontSize
|
|
876
|
+
this.typeface = typeface
|
|
877
|
+
color = colorFromFillOrStroke(styleObj)
|
|
878
|
+
textAlign = when (align) {
|
|
879
|
+
"center" -> Paint.Align.CENTER
|
|
880
|
+
"right" -> Paint.Align.RIGHT
|
|
881
|
+
else -> Paint.Align.LEFT
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
var x = positionObj.float("x")
|
|
886
|
+
var y = positionObj.float("y")
|
|
887
|
+
|
|
888
|
+
// Adjust for baseline.
|
|
889
|
+
val metrics = paint.fontMetrics
|
|
890
|
+
when (baseline) {
|
|
891
|
+
"top" -> y -= metrics.top
|
|
892
|
+
"middle" -> y -= (metrics.top + metrics.bottom) / 2
|
|
893
|
+
"bottom" -> y -= metrics.bottom
|
|
894
|
+
// "alphabetic" is the default baseline for drawText.
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (maxWidth != null) {
|
|
898
|
+
// Scale text to fit within maxWidth.
|
|
899
|
+
val textWidth = paint.measureText(text)
|
|
900
|
+
if (textWidth > maxWidth) {
|
|
901
|
+
paint.textScaleX = maxWidth / textWidth
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
drawCanvas.drawText(text, x, y, paint)
|
|
906
|
+
|
|
907
|
+
restoreDrawOptions(drawCanvas, saveCount)
|
|
908
|
+
targetView.commit()
|
|
909
|
+
call.resolve()
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ---- Drawing: Image ----
|
|
914
|
+
|
|
915
|
+
@PluginMethod
|
|
916
|
+
fun drawImage(call: PluginCall) {
|
|
917
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
918
|
+
call.reject("Missing canvasId")
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
val canvas = canvases[canvasId] ?: run {
|
|
922
|
+
call.reject("Canvas not found")
|
|
923
|
+
return
|
|
924
|
+
}
|
|
925
|
+
val destRectObj = call.getObject("destRect") ?: run {
|
|
926
|
+
call.reject("Missing destRect")
|
|
927
|
+
return
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
val drawOpts = call.getObject("drawOptions")
|
|
931
|
+
val layerId = drawOpts?.getString("layerId")
|
|
932
|
+
val imageObj = call.getObject("image")
|
|
933
|
+
val imageString = call.getString("image")
|
|
934
|
+
val srcRectObj = call.getObject("srcRect")
|
|
935
|
+
|
|
936
|
+
activity.runOnUiThread {
|
|
937
|
+
val targetView = layerId?.let { canvas.layers[it]?.view } ?: canvas.view
|
|
938
|
+
val drawCanvas = targetView.getDrawCanvas()
|
|
939
|
+
val saveCount = applyDrawOptions(drawCanvas, canvas, drawOpts)
|
|
940
|
+
|
|
941
|
+
var bitmap: Bitmap? = null
|
|
942
|
+
|
|
943
|
+
// Try to decode from base64 object.
|
|
944
|
+
if (imageObj != null) {
|
|
945
|
+
val base64 = imageObj.getString("base64")
|
|
946
|
+
if (base64 != null) {
|
|
947
|
+
try {
|
|
948
|
+
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
|
949
|
+
bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
950
|
+
} catch (_: Exception) {
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Try to load from URL string (only for local/data URIs on main thread).
|
|
956
|
+
if (bitmap == null && imageString != null) {
|
|
957
|
+
try {
|
|
958
|
+
if (imageString.startsWith("data:")) {
|
|
959
|
+
val commaIdx = imageString.indexOf(',')
|
|
960
|
+
if (commaIdx > 0) {
|
|
961
|
+
val base64Data = imageString.substring(commaIdx + 1)
|
|
962
|
+
val bytes = Base64.decode(base64Data, Base64.DEFAULT)
|
|
963
|
+
bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
} catch (_: Exception) {
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (bitmap != null) {
|
|
971
|
+
val destRect = rectFromObject(destRectObj)
|
|
972
|
+
|
|
973
|
+
if (srcRectObj != null) {
|
|
974
|
+
// Crop source bitmap then draw into dest.
|
|
975
|
+
val srcRect = Rect(
|
|
976
|
+
srcRectObj.int("x"),
|
|
977
|
+
srcRectObj.int("y"),
|
|
978
|
+
(srcRectObj.double("x") + srcRectObj.double("width")).toInt(),
|
|
979
|
+
(srcRectObj.double("y") + srcRectObj.double("height")).toInt()
|
|
980
|
+
)
|
|
981
|
+
val dst = Rect(
|
|
982
|
+
destRect.left.toInt(), destRect.top.toInt(),
|
|
983
|
+
destRect.right.toInt(), destRect.bottom.toInt()
|
|
984
|
+
)
|
|
985
|
+
drawCanvas.drawBitmap(bitmap, srcRect, dst, null)
|
|
986
|
+
} else {
|
|
987
|
+
val dst = Rect(
|
|
988
|
+
destRect.left.toInt(), destRect.top.toInt(),
|
|
989
|
+
destRect.right.toInt(), destRect.bottom.toInt()
|
|
990
|
+
)
|
|
991
|
+
drawCanvas.drawBitmap(bitmap, null, dst, null)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
bitmap.recycle()
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
restoreDrawOptions(drawCanvas, saveCount)
|
|
998
|
+
targetView.commit()
|
|
999
|
+
call.resolve()
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// ---- Drawing: Batch ----
|
|
1004
|
+
|
|
1005
|
+
@PluginMethod
|
|
1006
|
+
fun drawBatch(call: PluginCall) {
|
|
1007
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1008
|
+
call.reject("Missing canvasId")
|
|
1009
|
+
return
|
|
1010
|
+
}
|
|
1011
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1012
|
+
call.reject("Canvas not found")
|
|
1013
|
+
return
|
|
1014
|
+
}
|
|
1015
|
+
val commands = call.getArray("commands") ?: run {
|
|
1016
|
+
call.reject("Missing commands")
|
|
1017
|
+
return
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
activity.runOnUiThread {
|
|
1021
|
+
val paint = Paint().apply { isAntiAlias = true }
|
|
1022
|
+
|
|
1023
|
+
for (i in 0 until commands.length()) {
|
|
1024
|
+
val command = commands.getJSONObject(i) ?: continue
|
|
1025
|
+
val type = command.optString("type", "")
|
|
1026
|
+
val args = command.optJSONObject("args") ?: continue
|
|
1027
|
+
|
|
1028
|
+
val drawOptsObj = args.optJSONObject("drawOptions")
|
|
1029
|
+
val targetLayerId = drawOptsObj?.optString("layerId")
|
|
1030
|
+
val targetView =
|
|
1031
|
+
targetLayerId?.let { canvas.layers[it]?.view } ?: canvas.view
|
|
1032
|
+
val drawCanvas = targetView.getDrawCanvas()
|
|
1033
|
+
val drawOpts = drawOptsObj?.let { jsObjectFromJSON(it) }
|
|
1034
|
+
val saveCount = applyDrawOptions(drawCanvas, canvas, drawOpts)
|
|
1035
|
+
|
|
1036
|
+
when (type) {
|
|
1037
|
+
"rect" -> {
|
|
1038
|
+
val rectJson = args.optJSONObject("rect")
|
|
1039
|
+
if (rectJson != null) {
|
|
1040
|
+
val rect = rectFromJSON(rectJson)
|
|
1041
|
+
val cr = args.optDouble("cornerRadius", 0.0).toFloat()
|
|
1042
|
+
val fillJson = args.optJSONObject("fill")
|
|
1043
|
+
val strokeJson = args.optJSONObject("stroke")
|
|
1044
|
+
|
|
1045
|
+
fillJson?.let {
|
|
1046
|
+
val fillObj = jsObjectFromJSON(it)
|
|
1047
|
+
val gradient = extractGradient(fillObj)
|
|
1048
|
+
if (gradient != null) {
|
|
1049
|
+
paint.shader = createShader(gradient)
|
|
1050
|
+
} else {
|
|
1051
|
+
paint.shader = null
|
|
1052
|
+
paint.color = colorFromFillOrStroke(fillObj)
|
|
1053
|
+
}
|
|
1054
|
+
paint.style = Paint.Style.FILL
|
|
1055
|
+
if (cr > 0) drawCanvas.drawRoundRect(rect, cr, cr, paint)
|
|
1056
|
+
else drawCanvas.drawRect(rect, paint)
|
|
1057
|
+
paint.shader = null
|
|
1058
|
+
}
|
|
1059
|
+
strokeJson?.let {
|
|
1060
|
+
val strokeObj = jsObjectFromJSON(it)
|
|
1061
|
+
paint.color = colorFromFillOrStroke(strokeObj)
|
|
1062
|
+
paint.style = Paint.Style.STROKE
|
|
1063
|
+
paint.strokeWidth =
|
|
1064
|
+
strokeObj.float("width", 1f)
|
|
1065
|
+
applyStrokeStyle(paint, strokeObj)
|
|
1066
|
+
if (cr > 0) drawCanvas.drawRoundRect(rect, cr, cr, paint)
|
|
1067
|
+
else drawCanvas.drawRect(rect, paint)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
"ellipse" -> {
|
|
1072
|
+
val centerJson = args.optJSONObject("center")
|
|
1073
|
+
if (centerJson != null) {
|
|
1074
|
+
val cx = centerJson.optDouble("x", 0.0).toFloat()
|
|
1075
|
+
val cy = centerJson.optDouble("y", 0.0).toFloat()
|
|
1076
|
+
val rx = args.optDouble("radiusX", 0.0).toFloat()
|
|
1077
|
+
val ry = args.optDouble("radiusY", 0.0).toFloat()
|
|
1078
|
+
val ellRect =
|
|
1079
|
+
RectF(cx - rx, cy - ry, cx + rx, cy + ry)
|
|
1080
|
+
|
|
1081
|
+
args.optJSONObject("fill")?.let {
|
|
1082
|
+
val fillObj = jsObjectFromJSON(it)
|
|
1083
|
+
val gradient = extractGradient(fillObj)
|
|
1084
|
+
if (gradient != null) {
|
|
1085
|
+
paint.shader = createShader(gradient)
|
|
1086
|
+
} else {
|
|
1087
|
+
paint.shader = null
|
|
1088
|
+
paint.color = colorFromFillOrStroke(fillObj)
|
|
1089
|
+
}
|
|
1090
|
+
paint.style = Paint.Style.FILL
|
|
1091
|
+
drawCanvas.drawOval(ellRect, paint)
|
|
1092
|
+
paint.shader = null
|
|
1093
|
+
}
|
|
1094
|
+
args.optJSONObject("stroke")?.let {
|
|
1095
|
+
val strokeObj = jsObjectFromJSON(it)
|
|
1096
|
+
paint.color = colorFromFillOrStroke(strokeObj)
|
|
1097
|
+
paint.style = Paint.Style.STROKE
|
|
1098
|
+
paint.strokeWidth =
|
|
1099
|
+
strokeObj.float("width", 1f)
|
|
1100
|
+
applyStrokeStyle(paint, strokeObj)
|
|
1101
|
+
drawCanvas.drawOval(ellRect, paint)
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
"line" -> {
|
|
1106
|
+
val fromJson = args.optJSONObject("from")
|
|
1107
|
+
val toJson = args.optJSONObject("to")
|
|
1108
|
+
val strokeJson = args.optJSONObject("stroke")
|
|
1109
|
+
if (fromJson != null && toJson != null && strokeJson != null) {
|
|
1110
|
+
val strokeObj = jsObjectFromJSON(strokeJson)
|
|
1111
|
+
paint.color = colorFromFillOrStroke(strokeObj)
|
|
1112
|
+
paint.style = Paint.Style.STROKE
|
|
1113
|
+
paint.strokeWidth =
|
|
1114
|
+
strokeObj.float("width", 1f)
|
|
1115
|
+
applyStrokeStyle(paint, strokeObj)
|
|
1116
|
+
drawCanvas.drawLine(
|
|
1117
|
+
fromJson.optDouble("x", 0.0).toFloat(),
|
|
1118
|
+
fromJson.optDouble("y", 0.0).toFloat(),
|
|
1119
|
+
toJson.optDouble("x", 0.0).toFloat(),
|
|
1120
|
+
toJson.optDouble("y", 0.0).toFloat(),
|
|
1121
|
+
paint
|
|
1122
|
+
)
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
"path" -> {
|
|
1126
|
+
val pathJson = args.optJSONObject("path")
|
|
1127
|
+
val pathCommands = pathJson?.optJSONArray("commands")
|
|
1128
|
+
if (pathCommands != null) {
|
|
1129
|
+
val androidPath = buildPath(pathCommands)
|
|
1130
|
+
args.optJSONObject("fill")?.let {
|
|
1131
|
+
val fillObj = jsObjectFromJSON(it)
|
|
1132
|
+
val gradient = extractGradient(fillObj)
|
|
1133
|
+
if (gradient != null) {
|
|
1134
|
+
paint.shader = createShader(gradient)
|
|
1135
|
+
} else {
|
|
1136
|
+
paint.shader = null
|
|
1137
|
+
paint.color = colorFromFillOrStroke(fillObj)
|
|
1138
|
+
}
|
|
1139
|
+
paint.style = Paint.Style.FILL
|
|
1140
|
+
drawCanvas.drawPath(androidPath, paint)
|
|
1141
|
+
paint.shader = null
|
|
1142
|
+
}
|
|
1143
|
+
args.optJSONObject("stroke")?.let {
|
|
1144
|
+
val strokeObj = jsObjectFromJSON(it)
|
|
1145
|
+
paint.color = colorFromFillOrStroke(strokeObj)
|
|
1146
|
+
paint.style = Paint.Style.STROKE
|
|
1147
|
+
paint.strokeWidth =
|
|
1148
|
+
strokeObj.float("width", 1f)
|
|
1149
|
+
applyStrokeStyle(paint, strokeObj)
|
|
1150
|
+
drawCanvas.drawPath(androidPath, paint)
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
"text" -> {
|
|
1155
|
+
val textStr = args.optString("text", "")
|
|
1156
|
+
val posJson = args.optJSONObject("position")
|
|
1157
|
+
val styleJson = args.optJSONObject("style")
|
|
1158
|
+
if (textStr.isNotEmpty() && posJson != null && styleJson != null) {
|
|
1159
|
+
val styleObj = jsObjectFromJSON(styleJson)
|
|
1160
|
+
val textPaint = Paint().apply {
|
|
1161
|
+
isAntiAlias = true
|
|
1162
|
+
textSize = styleObj.float("size", 14f)
|
|
1163
|
+
color = colorFromFillOrStroke(styleObj)
|
|
1164
|
+
textAlign = when (styleObj.getString("align")) {
|
|
1165
|
+
"center" -> Paint.Align.CENTER
|
|
1166
|
+
"right" -> Paint.Align.RIGHT
|
|
1167
|
+
else -> Paint.Align.LEFT
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
drawCanvas.drawText(
|
|
1171
|
+
textStr,
|
|
1172
|
+
posJson.optDouble("x", 0.0).toFloat(),
|
|
1173
|
+
posJson.optDouble("y", 0.0).toFloat(),
|
|
1174
|
+
textPaint
|
|
1175
|
+
)
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
"image" -> {
|
|
1179
|
+
val destRectJson = args.optJSONObject("destRect")
|
|
1180
|
+
if (destRectJson != null) {
|
|
1181
|
+
val destRect = rectFromJSON(destRectJson)
|
|
1182
|
+
var bmp: Bitmap? = null
|
|
1183
|
+
val imgObj = args.optJSONObject("image")
|
|
1184
|
+
if (imgObj != null) {
|
|
1185
|
+
val b64 = imgObj.optString("base64", "")
|
|
1186
|
+
if (b64.isNotEmpty()) {
|
|
1187
|
+
try {
|
|
1188
|
+
val bytes = Base64.decode(b64, Base64.DEFAULT)
|
|
1189
|
+
bmp = BitmapFactory.decodeByteArray(
|
|
1190
|
+
bytes, 0, bytes.size
|
|
1191
|
+
)
|
|
1192
|
+
} catch (_: Exception) {
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (bmp != null) {
|
|
1197
|
+
val dst = Rect(
|
|
1198
|
+
destRect.left.toInt(), destRect.top.toInt(),
|
|
1199
|
+
destRect.right.toInt(), destRect.bottom.toInt()
|
|
1200
|
+
)
|
|
1201
|
+
drawCanvas.drawBitmap(bmp, null, dst, null)
|
|
1202
|
+
bmp.recycle()
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
"clear" -> {
|
|
1207
|
+
val clearRectJson = args.optJSONObject("rect")
|
|
1208
|
+
val clearLayerId = args.stringOrNull("layerId")
|
|
1209
|
+
val clearView =
|
|
1210
|
+
clearLayerId?.let { canvas.layers[it]?.view } ?: targetView
|
|
1211
|
+
if (clearRectJson != null) {
|
|
1212
|
+
clearView.clear(rectFromJSON(clearRectJson))
|
|
1213
|
+
} else {
|
|
1214
|
+
clearView.clear()
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
restoreDrawOptions(drawCanvas, saveCount)
|
|
1220
|
+
targetView.commit()
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
call.resolve()
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// ---- Pixel Data / Export ----
|
|
1228
|
+
|
|
1229
|
+
@PluginMethod
|
|
1230
|
+
fun getPixelData(call: PluginCall) {
|
|
1231
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1232
|
+
call.reject("Missing canvasId")
|
|
1233
|
+
return
|
|
1234
|
+
}
|
|
1235
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1236
|
+
call.reject("Canvas not found")
|
|
1237
|
+
return
|
|
1238
|
+
}
|
|
1239
|
+
val rectObj = call.getObject("rect")
|
|
1240
|
+
|
|
1241
|
+
activity.runOnUiThread {
|
|
1242
|
+
val bitmap = canvas.view.getBitmap()
|
|
1243
|
+
|
|
1244
|
+
val region = if (rectObj != null) {
|
|
1245
|
+
val x = rectObj.int("x")
|
|
1246
|
+
val y = rectObj.int("y")
|
|
1247
|
+
val w = rectObj.int("width", bitmap.width)
|
|
1248
|
+
val h = rectObj.int("height", bitmap.height)
|
|
1249
|
+
Rect(x, y, (x + w).coerceAtMost(bitmap.width), (y + h).coerceAtMost(bitmap.height))
|
|
1250
|
+
} else {
|
|
1251
|
+
Rect(0, 0, bitmap.width, bitmap.height)
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
val w = region.width()
|
|
1255
|
+
val h = region.height()
|
|
1256
|
+
if (w <= 0 || h <= 0) {
|
|
1257
|
+
call.reject("Invalid dimensions")
|
|
1258
|
+
return@runOnUiThread
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Extract RGBA pixel data.
|
|
1262
|
+
val pixels = IntArray(w * h)
|
|
1263
|
+
bitmap.getPixels(pixels, 0, w, region.left, region.top, w, h)
|
|
1264
|
+
|
|
1265
|
+
// Convert ARGB int array to RGBA byte array.
|
|
1266
|
+
val rgba = ByteArray(w * h * 4)
|
|
1267
|
+
for (i in pixels.indices) {
|
|
1268
|
+
val pixel = pixels[i]
|
|
1269
|
+
val offset = i * 4
|
|
1270
|
+
rgba[offset] = ((pixel shr 16) and 0xFF).toByte() // R
|
|
1271
|
+
rgba[offset + 1] = ((pixel shr 8) and 0xFF).toByte() // G
|
|
1272
|
+
rgba[offset + 2] = (pixel and 0xFF).toByte() // B
|
|
1273
|
+
rgba[offset + 3] = ((pixel shr 24) and 0xFF).toByte() // A
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
val base64 = Base64.encodeToString(rgba, Base64.NO_WRAP)
|
|
1277
|
+
|
|
1278
|
+
call.resolve(JSObject().apply {
|
|
1279
|
+
put("data", base64)
|
|
1280
|
+
put("width", w)
|
|
1281
|
+
put("height", h)
|
|
1282
|
+
})
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
@PluginMethod
|
|
1287
|
+
fun toImage(call: PluginCall) {
|
|
1288
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1289
|
+
call.reject("Missing canvasId")
|
|
1290
|
+
return
|
|
1291
|
+
}
|
|
1292
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1293
|
+
call.reject("Canvas not found")
|
|
1294
|
+
return
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
val format = call.getString("format") ?: "png"
|
|
1298
|
+
val quality = call.getInt("quality") ?: 100
|
|
1299
|
+
val layerIds = call.getArray("layerIds")
|
|
1300
|
+
|
|
1301
|
+
activity.runOnUiThread {
|
|
1302
|
+
val bitmap: Bitmap
|
|
1303
|
+
|
|
1304
|
+
if (layerIds != null && layerIds.length() > 0) {
|
|
1305
|
+
// Composite only specified layers.
|
|
1306
|
+
bitmap = Bitmap.createBitmap(
|
|
1307
|
+
canvas.size.width, canvas.size.height, Bitmap.Config.ARGB_8888
|
|
1308
|
+
)
|
|
1309
|
+
val compositeCanvas = Canvas(bitmap)
|
|
1310
|
+
for (i in 0 until layerIds.length()) {
|
|
1311
|
+
val lid = layerIds.getString(i)
|
|
1312
|
+
canvas.layers[lid]?.let { layer ->
|
|
1313
|
+
compositeCanvas.drawBitmap(layer.view.getBitmap(), 0f, 0f, null)
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
} else {
|
|
1317
|
+
// Draw the main canvas view plus all layers.
|
|
1318
|
+
bitmap = Bitmap.createBitmap(
|
|
1319
|
+
canvas.size.width, canvas.size.height, Bitmap.Config.ARGB_8888
|
|
1320
|
+
)
|
|
1321
|
+
val compositeCanvas = Canvas(bitmap)
|
|
1322
|
+
compositeCanvas.drawBitmap(canvas.view.getBitmap(), 0f, 0f, null)
|
|
1323
|
+
canvas.layers.values.sortedBy { it.zIndex }.forEach { layer ->
|
|
1324
|
+
if (layer.visible) {
|
|
1325
|
+
val layerPaint = Paint().apply { alpha = (layer.opacity * 255).toInt() }
|
|
1326
|
+
compositeCanvas.drawBitmap(
|
|
1327
|
+
layer.view.getBitmap(), 0f, 0f, layerPaint
|
|
1328
|
+
)
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
val outputStream = ByteArrayOutputStream()
|
|
1334
|
+
val compressFormat = when (format) {
|
|
1335
|
+
"jpeg" -> Bitmap.CompressFormat.JPEG
|
|
1336
|
+
"webp" -> if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
1337
|
+
Bitmap.CompressFormat.WEBP_LOSSY
|
|
1338
|
+
} else {
|
|
1339
|
+
@Suppress("DEPRECATION")
|
|
1340
|
+
Bitmap.CompressFormat.WEBP
|
|
1341
|
+
}
|
|
1342
|
+
else -> Bitmap.CompressFormat.PNG
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
bitmap.compress(compressFormat, quality, outputStream)
|
|
1346
|
+
val base64 = Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
|
1347
|
+
val outputFormat = if (format == "webp" &&
|
|
1348
|
+
android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R
|
|
1349
|
+
) "png" else format
|
|
1350
|
+
|
|
1351
|
+
bitmap.recycle()
|
|
1352
|
+
|
|
1353
|
+
call.resolve(JSObject().apply {
|
|
1354
|
+
put("base64", base64)
|
|
1355
|
+
put("format", outputFormat)
|
|
1356
|
+
put("width", canvas.size.width)
|
|
1357
|
+
put("height", canvas.size.height)
|
|
1358
|
+
})
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// ---- Transform ----
|
|
1363
|
+
|
|
1364
|
+
@PluginMethod
|
|
1365
|
+
fun setTransform(call: PluginCall) {
|
|
1366
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1367
|
+
call.reject("Missing canvasId")
|
|
1368
|
+
return
|
|
1369
|
+
}
|
|
1370
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1371
|
+
call.reject("Canvas not found")
|
|
1372
|
+
return
|
|
1373
|
+
}
|
|
1374
|
+
val transformObj = call.getObject("transform") ?: run {
|
|
1375
|
+
call.reject("Missing transform")
|
|
1376
|
+
return
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
canvas.globalTransform = matrixFromTransformObject(transformObj)
|
|
1380
|
+
call.resolve()
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
@PluginMethod
|
|
1384
|
+
fun resetTransform(call: PluginCall) {
|
|
1385
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1386
|
+
call.reject("Missing canvasId")
|
|
1387
|
+
return
|
|
1388
|
+
}
|
|
1389
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1390
|
+
call.reject("Canvas not found")
|
|
1391
|
+
return
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
canvas.globalTransform = Matrix()
|
|
1395
|
+
call.resolve()
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// ---- Touch ----
|
|
1399
|
+
|
|
1400
|
+
@PluginMethod
|
|
1401
|
+
fun setTouchEnabled(call: PluginCall) {
|
|
1402
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1403
|
+
call.reject("Missing canvasId")
|
|
1404
|
+
return
|
|
1405
|
+
}
|
|
1406
|
+
val enabled = call.getBoolean("enabled") ?: false
|
|
1407
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1408
|
+
call.reject("Canvas not found")
|
|
1409
|
+
return
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
activity.runOnUiThread {
|
|
1413
|
+
canvas.touchEnabled = enabled
|
|
1414
|
+
canvas.view.isClickable = enabled
|
|
1415
|
+
call.resolve()
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// ======== Web Canvas Operations ========
|
|
1420
|
+
|
|
1421
|
+
// ---- Navigate ----
|
|
1422
|
+
|
|
1423
|
+
@PluginMethod
|
|
1424
|
+
fun navigate(call: PluginCall) {
|
|
1425
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1426
|
+
call.reject("Missing canvasId")
|
|
1427
|
+
return
|
|
1428
|
+
}
|
|
1429
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1430
|
+
call.reject("Canvas not found")
|
|
1431
|
+
return
|
|
1432
|
+
}
|
|
1433
|
+
val urlString = call.getString("url") ?: run {
|
|
1434
|
+
call.reject("Missing url")
|
|
1435
|
+
return
|
|
1436
|
+
}
|
|
1437
|
+
val placementObj = call.getObject("placement")
|
|
1438
|
+
|
|
1439
|
+
activity.runOnUiThread {
|
|
1440
|
+
val wv = ensureWebView(canvas)
|
|
1441
|
+
|
|
1442
|
+
// Apply placement if provided, otherwise fill the canvas.
|
|
1443
|
+
if (placementObj != null) {
|
|
1444
|
+
val x = placementObj.float("x")
|
|
1445
|
+
val y = placementObj.float("y")
|
|
1446
|
+
val w = placementObj.float("width", canvas.size.width.toFloat())
|
|
1447
|
+
val h = placementObj.float("height", canvas.size.height.toFloat())
|
|
1448
|
+
wv.x = x
|
|
1449
|
+
wv.y = y
|
|
1450
|
+
wv.layoutParams = FrameLayout.LayoutParams(w.toInt(), h.toInt())
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
val trimmed = urlString.trim()
|
|
1454
|
+
wv.loadUrl(trimmed)
|
|
1455
|
+
|
|
1456
|
+
call.resolve(JSObject().apply {
|
|
1457
|
+
put("url", trimmed)
|
|
1458
|
+
})
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// ---- Eval ----
|
|
1463
|
+
|
|
1464
|
+
@PluginMethod
|
|
1465
|
+
fun eval(call: PluginCall) {
|
|
1466
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1467
|
+
call.reject("Missing canvasId")
|
|
1468
|
+
return
|
|
1469
|
+
}
|
|
1470
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1471
|
+
call.reject("Canvas not found")
|
|
1472
|
+
return
|
|
1473
|
+
}
|
|
1474
|
+
val script = call.getString("script") ?: run {
|
|
1475
|
+
call.reject("Missing script")
|
|
1476
|
+
return
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
val wv = canvas.webView ?: run {
|
|
1480
|
+
call.reject("No web view - call navigate() first")
|
|
1481
|
+
return
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
activity.runOnUiThread {
|
|
1485
|
+
wv.evaluateJavascript(script) { result ->
|
|
1486
|
+
call.resolve(JSObject().apply {
|
|
1487
|
+
put("result", result ?: "")
|
|
1488
|
+
})
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// ---- Snapshot ----
|
|
1494
|
+
|
|
1495
|
+
@PluginMethod
|
|
1496
|
+
fun snapshot(call: PluginCall) {
|
|
1497
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1498
|
+
call.reject("Missing canvasId")
|
|
1499
|
+
return
|
|
1500
|
+
}
|
|
1501
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1502
|
+
call.reject("Canvas not found")
|
|
1503
|
+
return
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
val wv = canvas.webView ?: run {
|
|
1507
|
+
call.reject("No web view - call navigate() first")
|
|
1508
|
+
return
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
val maxWidth = call.getFloat("maxWidth")
|
|
1512
|
+
val quality = call.getDouble("quality") ?: 0.82
|
|
1513
|
+
val formatStr = call.getString("format") ?: "png"
|
|
1514
|
+
|
|
1515
|
+
activity.runOnUiThread {
|
|
1516
|
+
// Capture the WebView as a bitmap (same approach as classic CanvasController).
|
|
1517
|
+
val width = wv.width.coerceAtLeast(1)
|
|
1518
|
+
val height = wv.height.coerceAtLeast(1)
|
|
1519
|
+
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
1520
|
+
val bitmapCanvas = Canvas(bitmap)
|
|
1521
|
+
wv.draw(bitmapCanvas)
|
|
1522
|
+
|
|
1523
|
+
// Scale if maxWidth specified.
|
|
1524
|
+
val scaled = if (maxWidth != null && maxWidth > 0 && bitmap.width > maxWidth) {
|
|
1525
|
+
val scale = maxWidth / bitmap.width
|
|
1526
|
+
val newH = (bitmap.height * scale).toInt().coerceAtLeast(1)
|
|
1527
|
+
Bitmap.createScaledBitmap(bitmap, maxWidth.toInt(), newH, true).also {
|
|
1528
|
+
if (it !== bitmap) bitmap.recycle()
|
|
1529
|
+
}
|
|
1530
|
+
} else {
|
|
1531
|
+
bitmap
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
val outputStream = ByteArrayOutputStream()
|
|
1535
|
+
val (compressFormat, compressQuality) = when (formatStr) {
|
|
1536
|
+
"jpeg" -> Bitmap.CompressFormat.JPEG to (quality * 100).toInt()
|
|
1537
|
+
.coerceIn(1, 100)
|
|
1538
|
+
else -> Bitmap.CompressFormat.PNG to 100
|
|
1539
|
+
}
|
|
1540
|
+
scaled.compress(compressFormat, compressQuality, outputStream)
|
|
1541
|
+
val base64 = Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
|
1542
|
+
val outputFormat = if (formatStr == "jpeg") "jpeg" else "png"
|
|
1543
|
+
|
|
1544
|
+
val resultWidth = scaled.width
|
|
1545
|
+
val resultHeight = scaled.height
|
|
1546
|
+
if (scaled !== bitmap) scaled.recycle()
|
|
1547
|
+
|
|
1548
|
+
call.resolve(JSObject().apply {
|
|
1549
|
+
put("base64", base64)
|
|
1550
|
+
put("format", outputFormat)
|
|
1551
|
+
put("width", resultWidth)
|
|
1552
|
+
put("height", resultHeight)
|
|
1553
|
+
})
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// ---- A2UI Push ----
|
|
1558
|
+
|
|
1559
|
+
@PluginMethod
|
|
1560
|
+
fun a2uiPush(call: PluginCall) {
|
|
1561
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1562
|
+
call.reject("Missing canvasId")
|
|
1563
|
+
return
|
|
1564
|
+
}
|
|
1565
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1566
|
+
call.reject("Canvas not found")
|
|
1567
|
+
return
|
|
1568
|
+
}
|
|
1569
|
+
val wv = canvas.webView ?: run {
|
|
1570
|
+
call.reject("No web view - call navigate() first")
|
|
1571
|
+
return
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Accept "messages" (JSON array), "jsonl" (newline-delimited JSON string), or "payload".
|
|
1575
|
+
val messagesJSON: String = when {
|
|
1576
|
+
call.getArray("messages") != null -> {
|
|
1577
|
+
call.getArray("messages").toString()
|
|
1578
|
+
}
|
|
1579
|
+
call.getString("jsonl") != null -> {
|
|
1580
|
+
val jsonl = call.getString("jsonl")!!
|
|
1581
|
+
val parsed = JSONArray()
|
|
1582
|
+
for (line in jsonl.split("\n")) {
|
|
1583
|
+
val trimmed = line.trim()
|
|
1584
|
+
if (trimmed.isNotEmpty()) {
|
|
1585
|
+
try {
|
|
1586
|
+
parsed.put(JSONObject(trimmed))
|
|
1587
|
+
} catch (e: Exception) {
|
|
1588
|
+
call.reject("Invalid JSONL at line: ${trimmed.take(80)}")
|
|
1589
|
+
return
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
parsed.toString()
|
|
1594
|
+
}
|
|
1595
|
+
call.getObject("payload") != null -> {
|
|
1596
|
+
val payload = call.getObject("payload")
|
|
1597
|
+
JSONArray().put(payload).toString()
|
|
1598
|
+
}
|
|
1599
|
+
else -> {
|
|
1600
|
+
call.reject("Missing messages, jsonl, or payload parameter")
|
|
1601
|
+
return
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
val escapedJSON = jsStringLiteral(messagesJSON)
|
|
1606
|
+
|
|
1607
|
+
val js = """
|
|
1608
|
+
(function() {
|
|
1609
|
+
try {
|
|
1610
|
+
var host = globalThis.elizaA2UI;
|
|
1611
|
+
if (host && typeof host.applyMessages === 'function') {
|
|
1612
|
+
host.applyMessages(JSON.parse($escapedJSON));
|
|
1613
|
+
return 'ok';
|
|
1614
|
+
}
|
|
1615
|
+
return 'a2ui_not_ready';
|
|
1616
|
+
} catch (e) {
|
|
1617
|
+
return 'error:' + e.message;
|
|
1618
|
+
}
|
|
1619
|
+
})()
|
|
1620
|
+
""".trimIndent()
|
|
1621
|
+
|
|
1622
|
+
activity.runOnUiThread {
|
|
1623
|
+
wv.evaluateJavascript(js) { result ->
|
|
1624
|
+
val resultStr = result?.replace("\"", "") ?: ""
|
|
1625
|
+
when {
|
|
1626
|
+
resultStr == "a2ui_not_ready" -> {
|
|
1627
|
+
call.reject("A2UI host not ready - ensure the canvas page includes the A2UI runtime")
|
|
1628
|
+
}
|
|
1629
|
+
resultStr.startsWith("error:") -> {
|
|
1630
|
+
call.reject("a2uiPush JS error: $resultStr")
|
|
1631
|
+
}
|
|
1632
|
+
else -> {
|
|
1633
|
+
call.resolve()
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// ---- A2UI Reset ----
|
|
1641
|
+
|
|
1642
|
+
@PluginMethod
|
|
1643
|
+
fun a2uiReset(call: PluginCall) {
|
|
1644
|
+
val canvasId = call.getString("canvasId") ?: run {
|
|
1645
|
+
call.reject("Missing canvasId")
|
|
1646
|
+
return
|
|
1647
|
+
}
|
|
1648
|
+
val canvas = canvases[canvasId] ?: run {
|
|
1649
|
+
call.reject("Canvas not found")
|
|
1650
|
+
return
|
|
1651
|
+
}
|
|
1652
|
+
val wv = canvas.webView ?: run {
|
|
1653
|
+
call.reject("No web view - call navigate() first")
|
|
1654
|
+
return
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
val js = """
|
|
1658
|
+
(function() {
|
|
1659
|
+
try {
|
|
1660
|
+
var host = globalThis.elizaA2UI;
|
|
1661
|
+
if (host && typeof host.reset === 'function') {
|
|
1662
|
+
host.reset();
|
|
1663
|
+
return 'ok';
|
|
1664
|
+
}
|
|
1665
|
+
return 'no_reset';
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
return 'error:' + e.message;
|
|
1668
|
+
}
|
|
1669
|
+
})()
|
|
1670
|
+
""".trimIndent()
|
|
1671
|
+
|
|
1672
|
+
activity.runOnUiThread {
|
|
1673
|
+
wv.evaluateJavascript(js) { result ->
|
|
1674
|
+
if (result != null && result.contains("error:")) {
|
|
1675
|
+
call.reject("a2uiReset failed: $result")
|
|
1676
|
+
} else {
|
|
1677
|
+
call.resolve()
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// ======== Web View Management ========
|
|
1684
|
+
|
|
1685
|
+
@android.annotation.SuppressLint("SetJavaScriptEnabled")
|
|
1686
|
+
private fun ensureWebView(canvas: ManagedCanvas): WebView {
|
|
1687
|
+
canvas.webView?.let { return it }
|
|
1688
|
+
|
|
1689
|
+
val wv = WebView(context).apply {
|
|
1690
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
1691
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
1692
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
1693
|
+
)
|
|
1694
|
+
settings.javaScriptEnabled = true
|
|
1695
|
+
settings.domStorageEnabled = true
|
|
1696
|
+
settings.mediaPlaybackRequiresUserGesture = false
|
|
1697
|
+
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
|
1698
|
+
settings.allowFileAccess = true
|
|
1699
|
+
setBackgroundColor(Color.BLACK)
|
|
1700
|
+
isVerticalScrollBarEnabled = true
|
|
1701
|
+
isHorizontalScrollBarEnabled = true
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// Set up the A2UI message bridge via JS interface.
|
|
1705
|
+
val canvasId = canvas.id
|
|
1706
|
+
val pluginRef = this
|
|
1707
|
+
wv.addJavascriptInterface(object {
|
|
1708
|
+
@JavascriptInterface
|
|
1709
|
+
fun postAction(actionJson: String) {
|
|
1710
|
+
try {
|
|
1711
|
+
val json = JSONObject(actionJson)
|
|
1712
|
+
val userAction = json.optJSONObject("userAction") ?: json
|
|
1713
|
+
val actionName = extractActionName(userAction)
|
|
1714
|
+
val actionId = userAction.optString("id", UUID.randomUUID().toString())
|
|
1715
|
+
val surfaceId = userAction.optString("surfaceId", "main")
|
|
1716
|
+
|
|
1717
|
+
Handler(Looper.getMainLooper()).post {
|
|
1718
|
+
pluginRef.notifyListeners("a2uiAction", JSObject().apply {
|
|
1719
|
+
put("canvasId", canvasId)
|
|
1720
|
+
put("actionId", actionId)
|
|
1721
|
+
put("actionName", actionName ?: "")
|
|
1722
|
+
put("surfaceId", surfaceId)
|
|
1723
|
+
put("userAction", jsObjectFromJSON(userAction))
|
|
1724
|
+
})
|
|
1725
|
+
|
|
1726
|
+
// Dispatch action status acknowledgement.
|
|
1727
|
+
val statusJS = """
|
|
1728
|
+
(function() {
|
|
1729
|
+
var detail = { id: ${jsStringLiteral(actionId)}, ok: true, error: '' };
|
|
1730
|
+
window.dispatchEvent(new CustomEvent('eliza:a2ui-action-status', { detail: detail }));
|
|
1731
|
+
})();
|
|
1732
|
+
""".trimIndent()
|
|
1733
|
+
wv.evaluateJavascript(statusJS, null)
|
|
1734
|
+
}
|
|
1735
|
+
} catch (_: Exception) {
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}, "elizaCanvasA2UIBridge")
|
|
1739
|
+
|
|
1740
|
+
// Navigation delegate: intercept eliza:// deep links, emit events.
|
|
1741
|
+
wv.webViewClient = object : WebViewClient() {
|
|
1742
|
+
override fun shouldOverrideUrlLoading(
|
|
1743
|
+
view: WebView,
|
|
1744
|
+
request: WebResourceRequest
|
|
1745
|
+
): Boolean {
|
|
1746
|
+
val url = request.url
|
|
1747
|
+
if (url.scheme?.lowercase() == "eliza") {
|
|
1748
|
+
pluginRef.notifyListeners("deepLink", JSObject().apply {
|
|
1749
|
+
put("canvasId", canvasId)
|
|
1750
|
+
put("url", url.toString())
|
|
1751
|
+
})
|
|
1752
|
+
return true
|
|
1753
|
+
}
|
|
1754
|
+
return false
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
override fun onPageFinished(view: WebView, url: String?) {
|
|
1758
|
+
super.onPageFinished(view, url)
|
|
1759
|
+
|
|
1760
|
+
// Inject the A2UI bridge script so web content can post actions.
|
|
1761
|
+
val bridgeScript = """
|
|
1762
|
+
(function() {
|
|
1763
|
+
if (!window.webkit) window.webkit = {};
|
|
1764
|
+
if (!window.webkit.messageHandlers) window.webkit.messageHandlers = {};
|
|
1765
|
+
window.webkit.messageHandlers.elizaCanvasA2UIAction = {
|
|
1766
|
+
postMessage: function(msg) {
|
|
1767
|
+
var json = typeof msg === 'string' ? msg : JSON.stringify(msg);
|
|
1768
|
+
elizaCanvasA2UIBridge.postAction(json);
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
})();
|
|
1772
|
+
""".trimIndent()
|
|
1773
|
+
view.evaluateJavascript(bridgeScript, null)
|
|
1774
|
+
|
|
1775
|
+
pluginRef.notifyListeners("webViewReady", JSObject().apply {
|
|
1776
|
+
put("canvasId", canvasId)
|
|
1777
|
+
put("url", url ?: "")
|
|
1778
|
+
})
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
override fun onReceivedError(
|
|
1782
|
+
view: WebView,
|
|
1783
|
+
request: WebResourceRequest,
|
|
1784
|
+
error: WebResourceError
|
|
1785
|
+
) {
|
|
1786
|
+
super.onReceivedError(view, request, error)
|
|
1787
|
+
pluginRef.notifyListeners("navigationError", JSObject().apply {
|
|
1788
|
+
put("canvasId", canvasId)
|
|
1789
|
+
put("error", error.description?.toString() ?: "Unknown error")
|
|
1790
|
+
put("url", request.url?.toString() ?: "")
|
|
1791
|
+
})
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
wv.webChromeClient = WebChromeClient()
|
|
1796
|
+
|
|
1797
|
+
canvas.webView = wv
|
|
1798
|
+
|
|
1799
|
+
// Insert the web view behind drawing layers in the canvas view hierarchy.
|
|
1800
|
+
if (canvas.view.parent != null) {
|
|
1801
|
+
wv.layoutParams = FrameLayout.LayoutParams(
|
|
1802
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
1803
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
1804
|
+
)
|
|
1805
|
+
val parent = canvas.view.parent as? ViewGroup
|
|
1806
|
+
parent?.addView(wv, 0)
|
|
1807
|
+
canvas.view.setBackgroundColor(Color.TRANSPARENT)
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
return wv
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// ======== Internal Helpers ========
|
|
1814
|
+
|
|
1815
|
+
// ---- Draw Options: shadow, blend mode, opacity, transform ----
|
|
1816
|
+
|
|
1817
|
+
/** Apply draw options and return the Canvas save count for later restore. */
|
|
1818
|
+
private fun applyDrawOptions(
|
|
1819
|
+
canvas: Canvas,
|
|
1820
|
+
managedCanvas: ManagedCanvas,
|
|
1821
|
+
options: JSObject?
|
|
1822
|
+
): Int {
|
|
1823
|
+
val saveCount = canvas.save()
|
|
1824
|
+
|
|
1825
|
+
// Global canvas transform.
|
|
1826
|
+
if (!managedCanvas.globalTransform.isIdentity) {
|
|
1827
|
+
canvas.concat(managedCanvas.globalTransform)
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
if (options == null) return saveCount
|
|
1831
|
+
|
|
1832
|
+
// Per-operation transform.
|
|
1833
|
+
val transformObj = options.getJSObject("transform")
|
|
1834
|
+
if (transformObj != null) {
|
|
1835
|
+
canvas.concat(matrixFromTransformObject(transformObj))
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// Blend mode via Paint is per-draw; opacity and shadow are applied via layer.
|
|
1839
|
+
val opacity = options.doubleOrNull("opacity")
|
|
1840
|
+
if (opacity != null && opacity < 1.0) {
|
|
1841
|
+
canvas.saveLayerAlpha(
|
|
1842
|
+
null, (opacity * 255).toInt().coerceIn(0, 255)
|
|
1843
|
+
)
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
return saveCount
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
private fun restoreDrawOptions(canvas: Canvas, saveCount: Int) {
|
|
1850
|
+
canvas.restoreToCount(saveCount)
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// ---- Shadow ----
|
|
1854
|
+
|
|
1855
|
+
private fun applyShadow(paint: Paint, shadowObj: JSObject?) {
|
|
1856
|
+
if (shadowObj == null) return
|
|
1857
|
+
val blur = shadowObj.float("blur")
|
|
1858
|
+
val offsetX = shadowObj.float("offsetX")
|
|
1859
|
+
val offsetY = shadowObj.float("offsetY")
|
|
1860
|
+
val colorObj = shadowObj.getJSObject("color")
|
|
1861
|
+
val colorStr = shadowObj.stringOrNull("color")
|
|
1862
|
+
val color = when {
|
|
1863
|
+
colorObj != null -> colorFromObject(colorObj)
|
|
1864
|
+
colorStr != null -> colorFromHexString(colorStr)
|
|
1865
|
+
else -> Color.argb(128, 0, 0, 0)
|
|
1866
|
+
}
|
|
1867
|
+
paint.setShadowLayer(blur, offsetX, offsetY, color)
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// ---- Blend Mode ----
|
|
1871
|
+
|
|
1872
|
+
private fun blendModeFromString(mode: String): PorterDuff.Mode {
|
|
1873
|
+
return when (mode) {
|
|
1874
|
+
"multiply" -> PorterDuff.Mode.MULTIPLY
|
|
1875
|
+
"screen" -> PorterDuff.Mode.SCREEN
|
|
1876
|
+
"overlay" -> PorterDuff.Mode.OVERLAY
|
|
1877
|
+
"darken" -> PorterDuff.Mode.DARKEN
|
|
1878
|
+
"lighten" -> PorterDuff.Mode.LIGHTEN
|
|
1879
|
+
else -> PorterDuff.Mode.SRC_OVER
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// ---- Stroke Style ----
|
|
1884
|
+
|
|
1885
|
+
private fun applyStrokeStyle(paint: Paint, strokeObj: JSObject) {
|
|
1886
|
+
val lineCap = strokeObj.getString("lineCap")
|
|
1887
|
+
paint.strokeCap = when (lineCap) {
|
|
1888
|
+
"round" -> Paint.Cap.ROUND
|
|
1889
|
+
"square" -> Paint.Cap.SQUARE
|
|
1890
|
+
else -> Paint.Cap.BUTT
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
val lineJoin = strokeObj.getString("lineJoin")
|
|
1894
|
+
paint.strokeJoin = when (lineJoin) {
|
|
1895
|
+
"round" -> Paint.Join.ROUND
|
|
1896
|
+
"bevel" -> Paint.Join.BEVEL
|
|
1897
|
+
else -> Paint.Join.MITER
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
val dashPattern = strokeObj.arrayOrNull("dashPattern")
|
|
1901
|
+
if (dashPattern != null && dashPattern.length() > 0) {
|
|
1902
|
+
val intervals = FloatArray(dashPattern.length()) {
|
|
1903
|
+
dashPattern.optDouble(it, 0.0).toFloat()
|
|
1904
|
+
}
|
|
1905
|
+
if (intervals.size >= 2) {
|
|
1906
|
+
paint.pathEffect = DashPathEffect(intervals, 0f)
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// ---- Gradient ----
|
|
1912
|
+
|
|
1913
|
+
private fun extractGradient(obj: JSObject): JSObject? {
|
|
1914
|
+
val type = obj.getString("type") ?: return null
|
|
1915
|
+
if (type == "linear" || type == "radial") return obj
|
|
1916
|
+
return null
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
private fun createShader(gradientObj: JSObject): Shader {
|
|
1920
|
+
val type = gradientObj.getString("type") ?: "linear"
|
|
1921
|
+
val stops = gradientObj.arrayOrNull("stops")
|
|
1922
|
+
|
|
1923
|
+
val colors = mutableListOf<Int>()
|
|
1924
|
+
val positions = mutableListOf<Float>()
|
|
1925
|
+
|
|
1926
|
+
if (stops != null) {
|
|
1927
|
+
for (i in 0 until stops.length()) {
|
|
1928
|
+
val stop = stops.getJSONObject(i)
|
|
1929
|
+
positions.add(stop.optDouble("offset", 0.0).toFloat())
|
|
1930
|
+
|
|
1931
|
+
val colorObj = stop.optJSONObject("color")
|
|
1932
|
+
val colorStr = stop.stringOrNull("color")
|
|
1933
|
+
val color = when {
|
|
1934
|
+
colorObj != null -> colorFromObject(jsObjectFromJSON(colorObj))
|
|
1935
|
+
colorStr != null && colorStr.startsWith("#") -> colorFromHexString(colorStr)
|
|
1936
|
+
else -> Color.BLACK
|
|
1937
|
+
}
|
|
1938
|
+
colors.add(color)
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
if (colors.isEmpty()) {
|
|
1943
|
+
colors.add(Color.BLACK)
|
|
1944
|
+
colors.add(Color.BLACK)
|
|
1945
|
+
positions.add(0f)
|
|
1946
|
+
positions.add(1f)
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
return when (type) {
|
|
1950
|
+
"radial" -> {
|
|
1951
|
+
val x0 = gradientObj.float("x0")
|
|
1952
|
+
val y0 = gradientObj.float("y0")
|
|
1953
|
+
val r1 = gradientObj.float("r1", 1f)
|
|
1954
|
+
RadialGradient(
|
|
1955
|
+
x0, y0, r1.coerceAtLeast(0.001f),
|
|
1956
|
+
colors.toIntArray(), positions.toFloatArray(),
|
|
1957
|
+
Shader.TileMode.CLAMP
|
|
1958
|
+
)
|
|
1959
|
+
}
|
|
1960
|
+
else -> {
|
|
1961
|
+
val x0 = gradientObj.float("x0")
|
|
1962
|
+
val y0 = gradientObj.float("y0")
|
|
1963
|
+
val x1 = gradientObj.float("x1")
|
|
1964
|
+
val y1 = gradientObj.float("y1")
|
|
1965
|
+
LinearGradient(
|
|
1966
|
+
x0, y0, x1, y1,
|
|
1967
|
+
colors.toIntArray(), positions.toFloatArray(),
|
|
1968
|
+
Shader.TileMode.CLAMP
|
|
1969
|
+
)
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// ---- Transform ----
|
|
1975
|
+
|
|
1976
|
+
private fun matrixFromTransformObject(obj: JSObject): Matrix {
|
|
1977
|
+
val m = Matrix()
|
|
1978
|
+
val tx = obj.float("translateX")
|
|
1979
|
+
val ty = obj.float("translateY")
|
|
1980
|
+
if (tx != 0f || ty != 0f) m.postTranslate(tx, ty)
|
|
1981
|
+
|
|
1982
|
+
val sx = obj.floatOrNull("scaleX")
|
|
1983
|
+
val sy = obj.floatOrNull("scaleY")
|
|
1984
|
+
if (sx != null || sy != null) {
|
|
1985
|
+
m.postScale(sx ?: 1f, sy ?: 1f)
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
val rotation = obj.floatOrNull("rotation")
|
|
1989
|
+
if (rotation != null && rotation != 0f) {
|
|
1990
|
+
m.postRotate(Math.toDegrees(rotation.toDouble()).toFloat())
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
val skewX = obj.floatOrNull("skewX")
|
|
1994
|
+
if (skewX != null && skewX != 0f) {
|
|
1995
|
+
val skewMatrix = Matrix()
|
|
1996
|
+
skewMatrix.setSkew(tan(skewX.toDouble()).toFloat(), 0f)
|
|
1997
|
+
m.postConcat(skewMatrix)
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
val skewY = obj.floatOrNull("skewY")
|
|
2001
|
+
if (skewY != null && skewY != 0f) {
|
|
2002
|
+
val skewMatrix = Matrix()
|
|
2003
|
+
skewMatrix.setSkew(0f, tan(skewY.toDouble()).toFloat())
|
|
2004
|
+
m.postConcat(skewMatrix)
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
return m
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// ---- Color Utilities ----
|
|
2011
|
+
|
|
2012
|
+
private fun colorFromObject(obj: JSObject?): Int {
|
|
2013
|
+
if (obj == null) return Color.BLACK
|
|
2014
|
+
val r = obj.int("r")
|
|
2015
|
+
val g = obj.int("g")
|
|
2016
|
+
val b = obj.int("b")
|
|
2017
|
+
val a = (obj.double("a", 1.0) * 255).toInt()
|
|
2018
|
+
return Color.argb(a, r, g, b)
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
/**
|
|
2022
|
+
* Extract a color from a fill or stroke style object. Handles both `{ color: ... }` wrappers
|
|
2023
|
+
* and direct color objects, as well as hex string colors.
|
|
2024
|
+
*/
|
|
2025
|
+
private fun colorFromFillOrStroke(obj: JSObject): Int {
|
|
2026
|
+
val colorObj = obj.getJSObject("color")
|
|
2027
|
+
if (colorObj != null) return colorFromObject(colorObj)
|
|
2028
|
+
|
|
2029
|
+
val colorStr = obj.getString("color")
|
|
2030
|
+
if (colorStr != null) return colorFromHexString(colorStr)
|
|
2031
|
+
|
|
2032
|
+
// Maybe the object itself is a color.
|
|
2033
|
+
if (obj.has("r")) return colorFromObject(obj)
|
|
2034
|
+
|
|
2035
|
+
return Color.BLACK
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
private fun colorFromHexString(hex: String): Int {
|
|
2039
|
+
var sanitized = hex.trim().removePrefix("#")
|
|
2040
|
+
return try {
|
|
2041
|
+
when (sanitized.length) {
|
|
2042
|
+
6 -> {
|
|
2043
|
+
val rgb = sanitized.toLong(16).toInt()
|
|
2044
|
+
Color.rgb(
|
|
2045
|
+
(rgb shr 16) and 0xFF,
|
|
2046
|
+
(rgb shr 8) and 0xFF,
|
|
2047
|
+
rgb and 0xFF
|
|
2048
|
+
)
|
|
2049
|
+
}
|
|
2050
|
+
8 -> {
|
|
2051
|
+
val rgba = sanitized.toLong(16)
|
|
2052
|
+
Color.argb(
|
|
2053
|
+
(rgba and 0xFF).toInt(),
|
|
2054
|
+
((rgba shr 24) and 0xFF).toInt(),
|
|
2055
|
+
((rgba shr 16) and 0xFF).toInt(),
|
|
2056
|
+
((rgba shr 8) and 0xFF).toInt()
|
|
2057
|
+
)
|
|
2058
|
+
}
|
|
2059
|
+
else -> Color.BLACK
|
|
2060
|
+
}
|
|
2061
|
+
} catch (_: Exception) {
|
|
2062
|
+
Color.BLACK
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// ---- Rect Utilities ----
|
|
2067
|
+
|
|
2068
|
+
private fun rectFromObject(obj: JSObject): RectF {
|
|
2069
|
+
val x = obj.float("x")
|
|
2070
|
+
val y = obj.float("y")
|
|
2071
|
+
val w = obj.float("width")
|
|
2072
|
+
val h = obj.float("height")
|
|
2073
|
+
return RectF(x, y, x + w, y + h)
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
private fun JSObject.boolean(name: String, defaultValue: Boolean = false): Boolean =
|
|
2077
|
+
booleanOrNull(name) ?: defaultValue
|
|
2078
|
+
|
|
2079
|
+
private fun JSObject.booleanOrNull(name: String): Boolean? =
|
|
2080
|
+
if (has(name) && !isNull(name)) optBoolean(name) else null
|
|
2081
|
+
|
|
2082
|
+
private fun JSObject.double(name: String, defaultValue: Double = 0.0): Double =
|
|
2083
|
+
doubleOrNull(name) ?: defaultValue
|
|
2084
|
+
|
|
2085
|
+
private fun JSObject.doubleOrNull(name: String): Double? =
|
|
2086
|
+
if (has(name) && !isNull(name)) optDouble(name) else null
|
|
2087
|
+
|
|
2088
|
+
private fun JSObject.float(name: String, defaultValue: Float = 0f): Float =
|
|
2089
|
+
doubleOrNull(name)?.toFloat() ?: defaultValue
|
|
2090
|
+
|
|
2091
|
+
private fun JSObject.floatOrNull(name: String): Float? = doubleOrNull(name)?.toFloat()
|
|
2092
|
+
|
|
2093
|
+
private fun JSObject.int(name: String, defaultValue: Int = 0): Int =
|
|
2094
|
+
intOrNull(name) ?: defaultValue
|
|
2095
|
+
|
|
2096
|
+
private fun JSObject.intOrNull(name: String): Int? =
|
|
2097
|
+
if (has(name) && !isNull(name)) optInt(name) else null
|
|
2098
|
+
|
|
2099
|
+
private fun JSObject.arrayOrNull(name: String): JSONArray? =
|
|
2100
|
+
if (has(name) && !isNull(name)) optJSONArray(name) else null
|
|
2101
|
+
|
|
2102
|
+
private fun JSObject.stringOrNull(name: String): String? =
|
|
2103
|
+
if (has(name) && !isNull(name)) opt(name)?.takeUnless { it === JSONObject.NULL }?.toString() else null
|
|
2104
|
+
|
|
2105
|
+
private fun JSONObject.stringOrNull(name: String): String? =
|
|
2106
|
+
if (has(name) && !isNull(name)) opt(name)?.takeUnless { it === JSONObject.NULL }?.toString() else null
|
|
2107
|
+
|
|
2108
|
+
private fun rectFromJSON(obj: JSONObject): RectF {
|
|
2109
|
+
val x = obj.optDouble("x", 0.0).toFloat()
|
|
2110
|
+
val y = obj.optDouble("y", 0.0).toFloat()
|
|
2111
|
+
val w = obj.optDouble("width", 0.0).toFloat()
|
|
2112
|
+
val h = obj.optDouble("height", 0.0).toFloat()
|
|
2113
|
+
return RectF(x, y, x + w, y + h)
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// ---- JSON Conversion ----
|
|
2117
|
+
|
|
2118
|
+
private fun jsObjectFromJSON(json: JSONObject): JSObject {
|
|
2119
|
+
val result = JSObject()
|
|
2120
|
+
val keys = json.keys()
|
|
2121
|
+
while (keys.hasNext()) {
|
|
2122
|
+
val key = keys.next()
|
|
2123
|
+
result.put(key, json.get(key))
|
|
2124
|
+
}
|
|
2125
|
+
return result
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// ---- A2UI Action Name ----
|
|
2129
|
+
|
|
2130
|
+
private fun extractActionName(userAction: JSONObject): String? {
|
|
2131
|
+
for (key in listOf("name", "action")) {
|
|
2132
|
+
val raw = userAction.optString(key, "").trim()
|
|
2133
|
+
if (raw.isNotEmpty()) return raw
|
|
2134
|
+
}
|
|
2135
|
+
return null
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// ---- JS String Escape (matches iOS jsStringLiteral) ----
|
|
2139
|
+
|
|
2140
|
+
private fun jsStringLiteral(value: String): String {
|
|
2141
|
+
val escaped = value
|
|
2142
|
+
.replace("\\", "\\\\")
|
|
2143
|
+
.replace("\"", "\\\"")
|
|
2144
|
+
.replace("\n", "\\n")
|
|
2145
|
+
.replace("\r", "\\r")
|
|
2146
|
+
.replace("\t", "\\t")
|
|
2147
|
+
return "\"$escaped\""
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// ---- Layer Sorting ----
|
|
2151
|
+
|
|
2152
|
+
private fun sortLayers(canvas: ManagedCanvas) {
|
|
2153
|
+
val parent = canvas.view.parent as? ViewGroup ?: return
|
|
2154
|
+
val sorted = canvas.layers.values.sortedBy { it.zIndex }
|
|
2155
|
+
sorted.forEachIndexed { index, layer ->
|
|
2156
|
+
parent.removeView(layer.view)
|
|
2157
|
+
// Offset by 1 if web view is at index 0 inside canvas.view.
|
|
2158
|
+
parent.addView(layer.view, index + 1)
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// ---- Lifecycle ----
|
|
2163
|
+
|
|
2164
|
+
override fun handleOnDestroy() {
|
|
2165
|
+
super.handleOnDestroy()
|
|
2166
|
+
canvases.values.forEach { canvas ->
|
|
2167
|
+
canvas.webView?.destroy()
|
|
2168
|
+
(canvas.view.parent as? ViewGroup)?.removeView(canvas.view)
|
|
2169
|
+
canvas.layers.values.forEach { layer ->
|
|
2170
|
+
(layer.view.parent as? ViewGroup)?.removeView(layer.view)
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
canvases.clear()
|
|
2174
|
+
}
|
|
2175
|
+
}
|