-
Notifications
You must be signed in to change notification settings - Fork 0
/
live2d_wrapper.py
186 lines (125 loc) · 4.71 KB
/
live2d_wrapper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""
# Live2D SDK 2/3/4 viewer brython script
# By: jupiterbjy
# Last Update: 2022.2.20
Rewritten code from javascript variant for flexibility.
To use this, you need to include following sources first.
<script src="https://cdnjs.cloudflare.com/ajax/libs/brython/3.10.4/brython.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/brython/3.10.4/brython_stdlib.min.js"></script>
<script src="https://cubism.live2d.com/sdk-web/cubismcore/live2dcubismcore.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/dylanNew/live2d/webgl/Live2D/lib/live2d.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/pixi.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pixi-live2d-display/dist/index.min.js"></script>
"""
# TODO: Display Animation name, file size and etc if viable
# TODO: Accept background png, background opacity via URL query
# TODO: Add UI-less mode to allow it used as browser source
from browser import document, window, timer, bind
from typing import Mapping, Callable, Any
import traceback
from bake_logger import logger
canvas_div = document["live2d_canvas"]
# noinspection PyUnresolvedReferences
pixi = window.PIXI
# before startup set pixi DPI - assuming this isn't zoomed-in start scenario
# https://stackoverflow.com/a/66864375/10909029
pixi.settings.RESOLUTION = window.devicePixelRatio
app = pixi.Application.new({
"view": canvas_div,
"transparent": True,
"autoStart": True,
"resizeTo": canvas_div
# "autoDensity": True,
})
class L2DNameSpace:
"""
Namespace for debugging
"""
last_source = None
current_model = None
last_hit_areas = None
canvas_div = None
window.L2DNameSpace = L2DNameSpace
def load_live2d(json_or_url: Mapping | str, callback: Callable):
if not json_or_url:
raise ValueError("No url is provided")
logger.debug(f"Loading {json_or_url}")
L2DNameSpace.last_source = json_or_url
# try to remove previous model, if any
# Can't rely on try-except here, it still propagates error message.
if L2DNameSpace.current_model is not None:
try:
app.stage.removeChildAt(0)
L2DNameSpace.current_model = None
except Exception as err:
logger.critical(err)
else:
logger.info("Unloaded previous model")
logger.info("Loading new model")
model = pixi.live2d.Live2DModel.fromSync(json_or_url)
model.once("load", lambda *_: model_load_callback(model, callback))
def model_load_callback(model, callback):
logger.debug("in callback")
L2DNameSpace.current_model = model
try:
app.stage.addChild(model)
model.on("hit", model_hit_callback_closure(model))
resize(model)
finally:
# Could pass param to callback for success/fail check I guess?
callback()
def resize(model=None):
if model is None:
model = L2DNameSpace.current_model
if not model:
return
# app.resizeTo = canvas_div
# reset scale
model.scale.set(1.0)
# calculate scale
scale_h = canvas_div.clientHeight / model.height
scale_w = canvas_div.clientWidth / model.width
scale = min(scale_w, scale_h)
model.scale.set(scale)
logger.debug(f"Scaled to {scale} - Canvas was {canvas_div.clientHeight} {canvas_div.clientWidth}")
# center the model
diff_x = (canvas_div.clientWidth - model.width) // 2
diff_y = (canvas_div.clientHeight - model.height) // 2
model.x = diff_x
model.y = diff_y
logger.debug(f"Offset to {diff_x} / {diff_y}")
logger.info("Model resized")
def model_hit_callback_closure(model):
def model_hit_callback(hit_areas):
L2DNameSpace.last_hit_areas = hit_areas
logger.info(f"Touch on {hit_areas}")
for hit_area in hit_areas:
match hit_area:
case "body":
# sdk 2
model.motion("tap_body")
case "Body":
# sdk3/4
model.motion("Tap")
case "head" | "Head":
# sdk 2 / sdk 3/4
model.expression()
case _:
# unknown area
logger.debug(f"Unregistered hit area {hit_area}, ignoring")
return model_hit_callback
def on_window_resize():
logger.debug("Pixi Resize triggered")
app.resizeTo = canvas_div
resize()
class ResizeTimer:
active_timer = None
refresh_delay = 300
@classmethod
def set_timer(cls):
if cls.active_timer is not None:
timer.clear_timeout(cls.active_timer)
cls.active_timer = timer.set_timeout(on_window_resize, cls.refresh_delay)
@bind(window, "resize")
def on_resize(*_):
ResizeTimer.set_timer()