feat: sync all remaining python source board configurations
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import * as path from 'path';
|
||||
import { FileTree, Debug } from 'mixly';
|
||||
import FileSystemFS from './filesystem-fs';
|
||||
|
||||
|
||||
export default class FileSystemFileTree extends FileTree {
|
||||
constructor() {
|
||||
super(new FileSystemFS());
|
||||
}
|
||||
|
||||
async readFolder(inPath) {
|
||||
let output = [];
|
||||
try {
|
||||
const fs = this.getFS();
|
||||
const status = await fs.isDirectory(inPath);
|
||||
if (!status) {
|
||||
return output;
|
||||
}
|
||||
const children = await fs.readDirectory(inPath);
|
||||
for (let data of children) {
|
||||
const dataPath = path.join(inPath, data);
|
||||
const isDirectory = await fs.isDirectory(dataPath);
|
||||
if (isDirectory) {
|
||||
const isDirEmpty = await fs.isDirectoryEmpty(dataPath);
|
||||
output.push({
|
||||
type: 'folder',
|
||||
id: dataPath,
|
||||
children: !isDirEmpty,
|
||||
title: `/${this.getRootFolderName()}${dataPath}`
|
||||
});
|
||||
} else {
|
||||
output.push({
|
||||
type: 'file',
|
||||
id: dataPath,
|
||||
children: false,
|
||||
title: `/${this.getRootFolderName()}${dataPath}`
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Debug.error(error);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
129
mixly/boards/default_src/python_pyodide/others/filesystem-fs.js
Normal file
129
mixly/boards/default_src/python_pyodide/others/filesystem-fs.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { WebAccessFS } from '@zenfs/dom';
|
||||
import { get, set } from 'idb-keyval';
|
||||
import { FS } from 'mixly';
|
||||
|
||||
|
||||
class WebAccessFSExt extends WebAccessFS {
|
||||
constructor(handle) {
|
||||
super(handle);
|
||||
}
|
||||
|
||||
async readFile(path) {
|
||||
const handle = await this.getHandle(path);
|
||||
if (handle instanceof window.FileSystemFileHandle) {
|
||||
const file = await handle.getFile();
|
||||
const text = await file.text();
|
||||
return text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class FileSystemFS extends FS {
|
||||
#fs_ = null;
|
||||
#encoder_ = new TextEncoder();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async showDirectoryPicker() {
|
||||
const directoryHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||||
const permissionStatus = await directoryHandle.requestPermission({ mode: 'readwrite' });
|
||||
if (permissionStatus !== 'granted') {
|
||||
throw new Error('readwrite access to directory not granted');
|
||||
}
|
||||
await this.setFSCache(directoryHandle);
|
||||
this.#fs_ = new WebAccessFSExt(directoryHandle);
|
||||
return directoryHandle;
|
||||
}
|
||||
|
||||
async loadFS() {
|
||||
let directoryHandle = await this.getFSCache();
|
||||
if (!directoryHandle) {
|
||||
return null;
|
||||
}
|
||||
const permissionStatus = await directoryHandle.requestPermission({ mode: 'readwrite' });
|
||||
if (permissionStatus !== 'granted') {
|
||||
throw new Error('readwrite access to directory not granted');
|
||||
}
|
||||
this.#fs_ = new WebAccessFSExt(directoryHandle);
|
||||
return directoryHandle;
|
||||
}
|
||||
|
||||
async getFSCache() {
|
||||
return get('mixly-pyodide-fs');
|
||||
}
|
||||
|
||||
async setFSCache(data) {
|
||||
await set('mixly-pyodide-fs', data);
|
||||
}
|
||||
|
||||
async createFile(filePath) {
|
||||
return this.#fs_.createFile(filePath, '');
|
||||
}
|
||||
|
||||
async readFile(path) {
|
||||
return this.#fs_.readFile(path);
|
||||
}
|
||||
|
||||
async writeFile(path, data) {
|
||||
const encodedArray = this.#encoder_.encode(data);
|
||||
return this.#fs_.writeFile(path, encodedArray);
|
||||
}
|
||||
|
||||
async isFile(path) {
|
||||
const stats = await this.#fs_.stat(path);
|
||||
if (stats && stats.mode === 33279) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async renameFile(oldFilePath, newFilePath) {
|
||||
return await this.#fs_.rename(oldFilePath, newFilePath);
|
||||
}
|
||||
|
||||
async moveFile(oldFilePath, newFilePath) {
|
||||
return this.renameFile(oldFilePath, newFilePath);
|
||||
}
|
||||
|
||||
async deleteFile(filePath) {
|
||||
return this.#fs_.unlink(filePath);
|
||||
}
|
||||
|
||||
async createDirectory(folderPath) {
|
||||
return this.#fs_.mkdir(folderPath, 0o777);
|
||||
}
|
||||
|
||||
async readDirectory(path) {
|
||||
const result = await this.#fs_.readdir(path);
|
||||
return result;
|
||||
}
|
||||
|
||||
async isDirectory(path) {
|
||||
const stats = await this.#fs_.stat(path);
|
||||
if (stats && stats.mode === 16895) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async isDirectoryEmpty(path) {
|
||||
const result = await this.readDirectory(path);
|
||||
return !result?.length;
|
||||
}
|
||||
|
||||
async renameDirectory(oldFolderPath, newFolderPath) {
|
||||
return this.#fs_.rename(oldFolderPath, newFolderPath);
|
||||
}
|
||||
|
||||
async moveDirectory(oldFolderPath, newFolderPath) {
|
||||
return this.#fs_.rename(oldFolderPath, newFolderPath);
|
||||
}
|
||||
|
||||
async deleteDirectory(folderPath) {
|
||||
return this.#fs_.rmdir(folderPath);
|
||||
}
|
||||
}
|
||||
333
mixly/boards/default_src/python_pyodide/others/loader.js
Normal file
333
mixly/boards/default_src/python_pyodide/others/loader.js
Normal file
@@ -0,0 +1,333 @@
|
||||
import NavExt from './nav-ext';
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
import './tensorflow';
|
||||
import './sound.js';
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
NavExt.init();
|
||||
window.tf = tf;
|
||||
|
||||
// let featureExtractor;
|
||||
// featureExtractor = await tf.loadGraphModel("../common/media/tfmodel/model.json");
|
||||
// window.featureExtractor = featureExtractor;
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modalOverlay').style.display = 'none';
|
||||
}
|
||||
|
||||
// 从IndexedDB删除单个模型
|
||||
async function deleteModel(modelName) {
|
||||
try {
|
||||
await tf.io.removeModel(`indexeddb://${modelName}`);
|
||||
// 从UI移除
|
||||
const modelItem = document.querySelector(`.model-item[data-model-name="${modelName}"]`);
|
||||
if (modelItem) modelItem.remove();
|
||||
} catch (error) {
|
||||
console.error('删除模型失败:', error);
|
||||
alert('删除模型失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示单个模型项
|
||||
function displayModelItem(modelName) {
|
||||
const modelsList = document.getElementById('imported-models');
|
||||
if ([...modelsList.children].some(item => item.dataset.modelName === modelName)) {
|
||||
return;
|
||||
}
|
||||
const modelItem = document.createElement('div');
|
||||
modelItem.className = 'model-item';
|
||||
modelItem.dataset.modelName = modelName;
|
||||
modelItem.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; border-bottom: 1px solid #eee;">
|
||||
<span style="font-size: 1em; color: #333;">${modelName}</span>
|
||||
<button class="delete-model" style="
|
||||
background: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s;
|
||||
">删除</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 绑定删除事件
|
||||
modelItem.querySelector('.delete-model').addEventListener('click', () => {
|
||||
deleteModel(modelName);
|
||||
});
|
||||
|
||||
modelsList.appendChild(modelItem);
|
||||
}
|
||||
|
||||
// 清空所有模型
|
||||
async function clearAllModels() {
|
||||
try {
|
||||
const modelInfos = await tf.io.listModels();
|
||||
const deletePromises = Object.keys(modelInfos)
|
||||
.map(path => path.replace('indexeddb://', ''))
|
||||
.map(modelName => tf.io.removeModel(`indexeddb://${modelName}`));
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
document.getElementById('imported-models').innerHTML = '';
|
||||
} catch (error) {
|
||||
console.error('清空模型失败:', error);
|
||||
alert('清空模型失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载并显示所有模型
|
||||
async function loadAndDisplayAllModels() {
|
||||
try {
|
||||
const modelInfos = await tf.io.listModels();
|
||||
document.getElementById('imported-models').innerHTML = '';
|
||||
for (const [path] of Object.entries(modelInfos)) {
|
||||
const modelName = path.replace('indexeddb://', '');
|
||||
displayModelItem(modelName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载模型列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function createModal() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'modalOverlay';
|
||||
Object.assign(overlay.style, {
|
||||
display: 'none',
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
zIndex: '20011216',
|
||||
pointerEvents: 'auto'
|
||||
});
|
||||
const content = document.createElement('div');
|
||||
Object.assign(content.style, {
|
||||
backgroundColor: 'white',
|
||||
width: '60%',
|
||||
maxHeight: '80%',
|
||||
margin: '12vh auto',
|
||||
padding: '20px 30px',
|
||||
borderRadius: '12px'
|
||||
});
|
||||
content.innerHTML = `
|
||||
<h2 style="margin-bottom: 20px;">选择本地模型</h2>
|
||||
<div style="margin-bottom: 25px; position: relative; min-height: 200px;"> <!-- 新增 min-height 和 position -->
|
||||
<input type="file" id="model-upload" accept=".json,.bin" multiple style="display: none;">
|
||||
<label for="model-upload" style="
|
||||
background: #1216ab;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
">选择模型文件</label>
|
||||
<div style="display: flex; gap: 10px; margin-top: 15px; align-items: center;">
|
||||
<span style="line-height: 36px;">导入模型名称:</span>
|
||||
<input type="text" id="model-name" placeholder="模型名称" value="my-model" style="
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
">
|
||||
<button id="model-handle" style="
|
||||
background: #1216ab;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
">保存模型</button>
|
||||
</div>
|
||||
<div style="margin-top: 15px; display: flex; gap: 20px;">
|
||||
<div id="json-status" style="
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
flex: 1;
|
||||
">
|
||||
<span>❌ 模型结构描述文件(model.json)</span>
|
||||
|
||||
<div style="color: #666; font-size: 0.9em; margin-top: 5px;">未选择</div>
|
||||
</div>
|
||||
<div id="weights-status" style="
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
flex: 1;
|
||||
">
|
||||
<span>❌ 权重文件(model.weights.bin)</span>
|
||||
<div style="color: #666; font-size: 0.9em; margin-top: 5px;">0 个已选择</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="output" style="margin-bottom: 15px; min-height: 40px;"></div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<h3>已导入模型</h3>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button id="refresh-models" style="
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
">刷新</button>
|
||||
<button id="clear-models" style="
|
||||
background: #f44336;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="imported-models" style="
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
">
|
||||
<div style="color: #666; font-style: italic;">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 15px;">
|
||||
<button class="close-btn" style="
|
||||
background: #e0e0e0;
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
overlay.appendChild(content);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
content.querySelector('.close-btn').addEventListener('click', closeModal);
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) closeModal();
|
||||
});
|
||||
// 获取DOM元素
|
||||
const modelUpload = document.getElementById('model-upload');
|
||||
const modelHandle = document.getElementById('model-handle');
|
||||
const outputDiv = document.getElementById('output');
|
||||
|
||||
let jsonFile = null;
|
||||
let weightFiles = [];
|
||||
|
||||
modelUpload.addEventListener('change', async (event) => {
|
||||
const files = event.target.files;
|
||||
|
||||
// 获取状态元素
|
||||
const jsonStatus = document.getElementById('json-status');
|
||||
const weightsStatus = document.getElementById('weights-status');
|
||||
|
||||
// 重置状态显示(保持完整文件名描述)
|
||||
jsonStatus.querySelector('span').textContent = '❌ 模型结构描述文件(model.json)';
|
||||
jsonStatus.querySelector('div').textContent = '未选择';
|
||||
weightsStatus.querySelector('span').textContent = '❌ 权重文件(model.weights.bin)';
|
||||
weightsStatus.querySelector('div').textContent = '0 个已选择';
|
||||
|
||||
// 分离 JSON 和权重文件
|
||||
weightFiles = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (files[i].name.endsWith('.json')) {
|
||||
jsonFile = files[i];
|
||||
} else {
|
||||
weightFiles.push(files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!jsonFile) {
|
||||
alert('未找到 model.json 文件');
|
||||
return;
|
||||
}
|
||||
|
||||
outputDiv.innerHTML = '正在处理上传的模型文件...';
|
||||
|
||||
if (jsonFile) {
|
||||
jsonStatus.querySelector('span').textContent = '✅ 模型结构描述文件(model.json)';
|
||||
jsonStatus.querySelector('div').textContent = '已选择';
|
||||
const modelName = jsonFile.name.replace('.json', '');
|
||||
document.getElementById('model-name').value = modelName;
|
||||
}
|
||||
|
||||
if (weightFiles.length > 0) {
|
||||
weightsStatus.querySelector('span').textContent = '✅ 权重文件(model.weights.bin)';
|
||||
weightsStatus.querySelector('div').textContent = `${weightFiles.length} 个已选择`;
|
||||
}
|
||||
});
|
||||
|
||||
modelHandle.addEventListener('click', async () => {
|
||||
try {
|
||||
const modelNameInput = document.getElementById('model-name');
|
||||
const modelName = modelNameInput.value || 'mixly-model';
|
||||
|
||||
const model = await tf.loadLayersModel(
|
||||
tf.io.browserFiles([jsonFile, ...weightFiles])
|
||||
);
|
||||
await model.save(`indexeddb://${modelName}`);
|
||||
loadAndDisplayAllModels();
|
||||
outputDiv.innerHTML = `模型已成功保存为 ${modelName}!`;
|
||||
} catch (error) {
|
||||
outputDiv.innerHTML = `保存模型出错: ${error.message}`;
|
||||
console.error(error);
|
||||
}
|
||||
})
|
||||
|
||||
content.querySelector('#refresh-models').addEventListener('click', loadAndDisplayAllModels);
|
||||
content.querySelector('#clear-models').addEventListener('click', async () => {
|
||||
if (confirm('确定要删除所有模型吗?此操作不可恢复!')) {
|
||||
await clearAllModels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createModal();
|
||||
|
||||
// 使用立即执行的异步函数,避免 top-level await 导致模块异步加载
|
||||
(async () => {
|
||||
await loadAndDisplayAllModels();
|
||||
})();
|
||||
|
||||
function openModal() {
|
||||
loadAndDisplayAllModels();
|
||||
document.getElementById('modalOverlay').style.display = 'block';
|
||||
}
|
||||
|
||||
const workspace = Blockly.getMainWorkspace();
|
||||
workspace.registerButtonCallback('handleModels', function () {
|
||||
openModal();
|
||||
});
|
||||
|
||||
|
||||
async function prepare_qmyixtxi(imgTensor) {
|
||||
let net = null;
|
||||
|
||||
if (window.featureExtractor) {
|
||||
net = window.featureExtractor;
|
||||
} else {
|
||||
net = await tf.loadGraphModel("../common/media/tfmodel/model.json");
|
||||
window.featureExtractor = net;
|
||||
}
|
||||
const preprocessedImg = imgTensor
|
||||
.resizeBilinear([224, 224])
|
||||
.toFloat()
|
||||
.div(tf.scalar(127.5))
|
||||
.sub(tf.scalar(1))
|
||||
.expandDims(0);
|
||||
|
||||
const features = window.featureExtractor.predict(preprocessedImg);
|
||||
|
||||
let activation = features;
|
||||
return activation;
|
||||
}
|
||||
window.prepare_qmyixtxi = prepare_qmyixtxi;
|
||||
43
mixly/boards/default_src/python_pyodide/others/nav-ext.js
Normal file
43
mixly/boards/default_src/python_pyodide/others/nav-ext.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { app, Nav, Debug } from 'mixly';
|
||||
import * as Blockly from 'blockly/core';
|
||||
import PythonShell from './python-shell';
|
||||
|
||||
const NavExt = {};
|
||||
|
||||
NavExt.init = async function () {
|
||||
const nav = app.getNav();
|
||||
|
||||
nav.register({
|
||||
icon: 'icon-play-circled',
|
||||
title: '',
|
||||
id: 'python-run-btn',
|
||||
displayText: Blockly.Msg.MSG['run'],
|
||||
preconditionFn: () => {
|
||||
return true;
|
||||
},
|
||||
callback: () => {
|
||||
PythonShell.run().catch(Debug.error);
|
||||
},
|
||||
scopeType: Nav.Scope.LEFT,
|
||||
weight: 4
|
||||
});
|
||||
|
||||
nav.register({
|
||||
icon: 'icon-cancel',
|
||||
title: '',
|
||||
id: 'python-stop-btn',
|
||||
displayText: Blockly.Msg.MSG['stop'],
|
||||
preconditionFn: () => {
|
||||
return true;
|
||||
},
|
||||
callback: () => {
|
||||
PythonShell.stop().catch(Debug.error);
|
||||
},
|
||||
scopeType: Nav.Scope.LEFT,
|
||||
weight: 5
|
||||
});
|
||||
|
||||
await PythonShell.init();
|
||||
}
|
||||
|
||||
export default NavExt;
|
||||
664
mixly/boards/default_src/python_pyodide/others/pixi-sprite.js
Normal file
664
mixly/boards/default_src/python_pyodide/others/pixi-sprite.js
Normal file
@@ -0,0 +1,664 @@
|
||||
import PIXI from 'pixi.js';
|
||||
import $ from 'jquery';
|
||||
|
||||
|
||||
const mixlySprite = {
|
||||
stage: new PIXI.Container(),
|
||||
pointer: { x: 0, y: 0 },
|
||||
backgroundSprite: null,
|
||||
sprites: {},
|
||||
texts: {},
|
||||
counter: 0,
|
||||
keys: {},
|
||||
state: false,
|
||||
running: false,
|
||||
repeatPlay: () => { },
|
||||
|
||||
displayTag: false,
|
||||
processingDisplayEvent: null,
|
||||
|
||||
successDisplayEvents: [],
|
||||
successProcessingDisplayEvents: [],
|
||||
|
||||
startTime: performance.now(),
|
||||
timer: 0,
|
||||
lastFrameTime: null,
|
||||
lastSecond: null,
|
||||
targetFPS: 60,
|
||||
frameCount: 0,
|
||||
currentFPS: 60,
|
||||
|
||||
canvasHeight: 450,
|
||||
canvasWidth: 800
|
||||
};
|
||||
|
||||
mixlySprite.gameLoop = () => {
|
||||
if (mixlySprite.state == true) {
|
||||
mixlySprite.repeatPlay();
|
||||
mixlySprite.gameLoopDisplay();
|
||||
}
|
||||
mixlySprite.timer = performance.now() - mixlySprite.startTime;
|
||||
}
|
||||
mixlySprite.animate = (currentTime) => {
|
||||
const deltaTime = currentTime - mixlySprite.lastFrameTime;
|
||||
|
||||
if (deltaTime >= 1000 / mixlySprite.targetFPS) {
|
||||
mixlySprite.frameCount++;
|
||||
mixlySprite.gameLoop();
|
||||
mixlySprite.renderer.render(mixlySprite.stage);
|
||||
mixlySprite.lastFrameTime = currentTime;
|
||||
}
|
||||
if (currentTime - mixlySprite.lastSecond >= 1000) {
|
||||
mixlySprite.currentFPS = mixlySprite.frameCount;
|
||||
mixlySprite.frameCount = 0;
|
||||
mixlySprite.lastSecond = currentTime;
|
||||
}
|
||||
requestAnimationFrame(mixlySprite.animate);
|
||||
}
|
||||
|
||||
mixlySprite.createBackground = (img, mode = 0) => {
|
||||
var player;
|
||||
if (mode == 0) {
|
||||
// eslint-disable-next-line new-cap
|
||||
player = new PIXI.Sprite.fromImage(`../common/media/spriteimg/${img}.png`);
|
||||
}
|
||||
player.name = 'background';
|
||||
player.anchor.set(0.5);
|
||||
player.x = mixlySprite.canvasWidth / 2;
|
||||
player.y = mixlySprite.canvasHeight / 2;
|
||||
|
||||
// const $canvas = $('#spriteContainer canvas');
|
||||
// const canvasWidth = $canvas.width();
|
||||
// const canvasHeight = $canvas.height();
|
||||
// player.width = ($('body').width() / 2);
|
||||
// player.height = ($('body').width() / 2)/canvasWidth*canvasHeight;
|
||||
|
||||
player.width = mixlySprite.canvasWidth;
|
||||
player.height = mixlySprite.canvasHeight;
|
||||
|
||||
player.interactive = true;
|
||||
player.buttonMode = true;
|
||||
player.isDown = false;
|
||||
player.isUp = true;
|
||||
player.on('mousedown', function () {
|
||||
this.isDown = true;
|
||||
this.isUp = false;
|
||||
if (mixlySprite.state) this.runningMouseDown();
|
||||
})
|
||||
.on('mouseup', function () {
|
||||
this.isDown = false;
|
||||
this.isUp = true;
|
||||
})
|
||||
.on('mouseupoutside', function () {
|
||||
this.isDown = false;
|
||||
this.isUp = true;
|
||||
});
|
||||
player.runningMouseDown = new Function("");
|
||||
if (mixlySprite.backgroundSprite && mixlySprite.backgroundSprite.parent) {
|
||||
// 如果子节点已经在父节点中,需要先移除
|
||||
mixlySprite.stage.removeChild(mixlySprite.backgroundSprite);
|
||||
}
|
||||
mixlySprite.backgroundSprite = player;
|
||||
mixlySprite.stage.addChildAt(mixlySprite.backgroundSprite, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
mixlySprite.createASprite = (img, x = mixlySprite.canvasWidth / 2, y = mixlySprite.canvasHeight / 2, name = '', mode = 0) => {
|
||||
if (name == '') {
|
||||
name = 'sprite' + (++mixlySprite.counter);
|
||||
}
|
||||
var player;
|
||||
if (mode == 0) {
|
||||
// eslint-disable-next-line new-cap
|
||||
player = new PIXI.Sprite.fromImage(`../common/media/spriteimg/${img}.png`);
|
||||
}
|
||||
if (!mixlySprite.sprites[name] && !mixlySprite.texts[name]) {
|
||||
player.name = name;
|
||||
player.anchor.set(0.5);
|
||||
player.x = x;
|
||||
player.y = y;
|
||||
player.interactive = true;
|
||||
player.buttonMode = true;
|
||||
player.isDown = false;
|
||||
player.isUp = true;
|
||||
player.on('mousedown', function (event) {
|
||||
this.isDown = true;
|
||||
this.isUp = false;
|
||||
if (!mixlySprite.state) {
|
||||
this.data = event.data;
|
||||
this.alpha = 0.5;
|
||||
this.dragging = true;
|
||||
} else this.runningMouseDown();
|
||||
})
|
||||
.on('mouseup', function () {
|
||||
this.isDown = false;
|
||||
this.isUp = true;
|
||||
if (!mixlySprite.state) {
|
||||
this.alpha = 1;
|
||||
this.dragging = false;
|
||||
this.data = null;
|
||||
}
|
||||
})
|
||||
.on('mouseupoutside', function () {
|
||||
this.isDown = false;
|
||||
this.isUp = true;
|
||||
if (!mixlySprite.state) {
|
||||
this.alpha = 1;
|
||||
this.dragging = false;
|
||||
this.data = null;
|
||||
}
|
||||
})
|
||||
.on('mousemove', function () {
|
||||
if (!mixlySprite.state)
|
||||
if (this.dragging) {
|
||||
var newPosition = this.data.getLocalPosition(this.parent);
|
||||
this.position.x = newPosition.x;
|
||||
this.position.y = newPosition.y;
|
||||
}
|
||||
})
|
||||
player.runningMouseDown = new Function("");
|
||||
player.show = function () {
|
||||
this.visible = true;
|
||||
};
|
||||
player.hide = function () {
|
||||
this.visible = false;
|
||||
};
|
||||
player.enlarge = function (s) {
|
||||
const ratio = this.height / this.width;
|
||||
var measure = Math.sqrt(this.height * this.width);
|
||||
measure += s;
|
||||
this.width = Math.sqrt(measure * measure / ratio);
|
||||
this.height = this.width * ratio;
|
||||
};
|
||||
player.enlargeTo = function (s) {
|
||||
var ratio = this.height / this.width;
|
||||
this.width = Math.sqrt(s * s / ratio);
|
||||
this.height = this.width * ratio;
|
||||
};
|
||||
|
||||
player.expandTo = async function (s, time = 1) {
|
||||
if (mixlySprite.running) {
|
||||
mixlySprite.displayTag = true;
|
||||
mixlySprite.processingDisplayEvent = {
|
||||
sprite: this,
|
||||
targetS: s,
|
||||
totalTime: time * 1000,
|
||||
startTime: performance.now(),
|
||||
displayType: 'expand'
|
||||
};
|
||||
|
||||
var prom = new Promise((resolve) => {
|
||||
if (mixlySprite.displayTag === false) {
|
||||
resolve();
|
||||
} else {
|
||||
const checkTagInterval = setInterval(() => {
|
||||
if (mixlySprite.displayTag === false) {
|
||||
clearInterval(checkTagInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
return await prom;
|
||||
}
|
||||
mixlySprite.successProcessingDisplayEvents.push({
|
||||
sprite: this,
|
||||
targetS: s,
|
||||
totalTime: time * 1000,
|
||||
startTime: performance.now(),
|
||||
displayType: 'expand'
|
||||
});
|
||||
return 0;
|
||||
};
|
||||
|
||||
player.move = function (step) {
|
||||
this.x += step * Math.cos(this.rotation);
|
||||
this.y += step * Math.sin(this.rotation);
|
||||
};
|
||||
player.moveTo = function (x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
};
|
||||
player.slideTo = async function (x, y, time = 1) {
|
||||
if (mixlySprite.running) {
|
||||
mixlySprite.displayTag = true;
|
||||
mixlySprite.processingDisplayEvent = {
|
||||
sprite: this,
|
||||
targetX: x,
|
||||
targetY: y,
|
||||
totalTime: time * 1000,
|
||||
startTime: performance.now(),
|
||||
displayType: 'slide'
|
||||
};
|
||||
|
||||
var prom = new Promise((resolve) => {
|
||||
const checkTagInterval = setInterval(() => {
|
||||
if (mixlySprite.displayTag === false) {
|
||||
clearInterval(checkTagInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
|
||||
return await prom;
|
||||
}
|
||||
mixlySprite.successProcessingDisplayEvents.push({
|
||||
sprite: this,
|
||||
targetX: x,
|
||||
targetY: y,
|
||||
totalTime: time * 1000,
|
||||
startTime: performance.now(),
|
||||
displayType: 'slide'
|
||||
});
|
||||
return 0;
|
||||
};
|
||||
player.addX = function (step) {
|
||||
this.x += step;
|
||||
};
|
||||
player.addY = function (step) {
|
||||
this.y += step;
|
||||
};
|
||||
player.rotate = function (degree) {
|
||||
this.rotation += Math.PI / 180 * degree;
|
||||
};
|
||||
player.rotateTo = function (degree) {
|
||||
this.rotation = Math.PI / 180 * degree;
|
||||
};
|
||||
player.circleTo = async function (degree, time = 1) {
|
||||
if (mixlySprite.running) {
|
||||
mixlySprite.displayTag = true;
|
||||
mixlySprite.processingDisplayEvent = {
|
||||
sprite: this,
|
||||
targetDegree: degree,
|
||||
totalTime: time * 1000,
|
||||
startTime: performance.now(),
|
||||
displayType: 'circle'
|
||||
};
|
||||
var prom = new Promise((resolve) => {
|
||||
if (mixlySprite.displayTag === false) {
|
||||
resolve();
|
||||
} else {
|
||||
const checkTagInterval = setInterval(() => {
|
||||
if (mixlySprite.displayTag === false) {
|
||||
clearInterval(checkTagInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
return await prom;
|
||||
}
|
||||
mixlySprite.successProcessingDisplayEvents.push({
|
||||
sprite: this,
|
||||
targetDegree: degree,
|
||||
totalTime: time * 1000,
|
||||
startTime: performance.now(),
|
||||
displayType: 'circle'
|
||||
});
|
||||
return 0;
|
||||
};
|
||||
player.hit = function (sprite) {
|
||||
return mixlySprite.hitTestRectangle(this, sprite);
|
||||
};
|
||||
player.outOfScreen = function () {
|
||||
return this.y >= mixlySprite.renderer.height || this.y <= 0 || this.x <= 0 || this.x >= mixlySprite.renderer.width;
|
||||
};
|
||||
player.mouseAction = function (func) {
|
||||
this.runningMouseDown = func;
|
||||
};
|
||||
|
||||
// new
|
||||
player.setScale = function (h = 0, w = 0) {
|
||||
if (h == 0) h = this.height;
|
||||
if (w == 0) w = this.width;
|
||||
this.height = h;
|
||||
this.width = w;
|
||||
}
|
||||
player.filterGray = function () {
|
||||
const grayscaleFilter = new PIXI.filters.ColorMatrixFilter();
|
||||
grayscaleFilter.blackAndWhite();
|
||||
this.filters = [grayscaleFilter];
|
||||
}
|
||||
player.filterBrighter = function () {
|
||||
const brightnessFilter = new PIXI.filters.ColorMatrixFilter();
|
||||
brightnessFilter.brightness(1.25); // 增加亮度
|
||||
this.filters = [brightnessFilter];
|
||||
}
|
||||
player.filterOrigin = function () {
|
||||
this.filters = null;
|
||||
}
|
||||
|
||||
mixlySprite.stage.addChild(player);
|
||||
mixlySprite.sprites[name] = player;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
mixlySprite.clearAllSprites = () => {
|
||||
if (mixlySprite.backgroundSprite && mixlySprite.backgroundSprite.parent) mixlySprite.backgroundSprite.parent.removeChild(mixlySprite.backgroundSprite);
|
||||
for (const name in mixlySprite.sprites) {
|
||||
mixlySprite.sprites[name].parent.removeChild(mixlySprite.sprites[name]);
|
||||
delete mixlySprite.sprites[name];
|
||||
}
|
||||
for (const name in mixlySprite.texts) {
|
||||
mixlySprite.texts[name].parent.removeChild(mixlySprite.texts[name]);
|
||||
delete mixlySprite.texts[name];
|
||||
}
|
||||
mixlySprite.counter = 0;
|
||||
mixlySprite.clearTimer();
|
||||
return 0;
|
||||
}
|
||||
|
||||
mixlySprite.createText = (text, x = mixlySprite.canvasWidth / 2, y = mixlySprite.canvasHeight / 2, name = '') => {
|
||||
if (name == '') {
|
||||
name = 'text' + (++mixlySprite.counter);
|
||||
}
|
||||
if (!mixlySprite.sprites[name] && !mixlySprite.texts[name]) {
|
||||
var textObj = new PIXI.Text(text);
|
||||
textObj.name = name;
|
||||
textObj.x = x;
|
||||
textObj.y = y;
|
||||
textObj.interactive = true;
|
||||
textObj.buttonMode = true;
|
||||
textObj.on('mousedown', function (event) {
|
||||
this.isDown = true;
|
||||
this.isUp = false;
|
||||
if (!mixlySprite.state) {
|
||||
this.data = event.data;
|
||||
this.alpha = 0.5;
|
||||
this.dragging = true;
|
||||
} else this.runningMouseDown();
|
||||
})
|
||||
.on('mouseup', function () {
|
||||
this.isDown = false;
|
||||
this.isUp = true;
|
||||
if (!mixlySprite.state) {
|
||||
this.alpha = 1;
|
||||
this.dragging = false;
|
||||
this.data = null;
|
||||
}
|
||||
})
|
||||
.on('mouseupoutside', function () {
|
||||
this.isDown = false;
|
||||
this.isUp = true;
|
||||
if (!mixlySprite.state) {
|
||||
this.alpha = 1;
|
||||
this.dragging = false;
|
||||
this.data = null;
|
||||
}
|
||||
})
|
||||
.on('mousemove', function () {
|
||||
if (!mixlySprite.state)
|
||||
if (this.dragging) {
|
||||
var newPosition = this.data.getLocalPosition(this.parent);
|
||||
this.position.x = newPosition.x;
|
||||
this.position.y = newPosition.y;
|
||||
}
|
||||
});
|
||||
textObj.runningMouseDown = new Function("");
|
||||
textObj.changeText = function (text) {
|
||||
this.text = text;
|
||||
};
|
||||
textObj.show = function () {
|
||||
this.visible = true;
|
||||
};
|
||||
textObj.hide = function () {
|
||||
this.visible = false;
|
||||
};
|
||||
mixlySprite.stage.addChild(textObj);
|
||||
mixlySprite.texts[name] = textObj;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
mixlySprite.hitTestRectangle = (r1, r2) => {
|
||||
let hit, combinedHalfWidths, combinedHalfHeights, vx, vy;
|
||||
hit = false;
|
||||
r1.centerX = r1.x + r1.width / 2;
|
||||
r1.centerY = r1.y + r1.height / 2;
|
||||
r2.centerX = r2.x + r2.width / 2;
|
||||
r2.centerY = r2.y + r2.height / 2;
|
||||
r1.halfWidth = r1.width / 2;
|
||||
r1.halfHeight = r1.height / 2;
|
||||
r2.halfWidth = r2.width / 2;
|
||||
r2.halfHeight = r2.height / 2;
|
||||
vx = r1.centerX - r2.centerX;
|
||||
vy = r1.centerY - r2.centerY;
|
||||
combinedHalfWidths = r1.halfWidth + r2.halfWidth;
|
||||
combinedHalfHeights = r1.halfHeight + r2.halfHeight;
|
||||
if (Math.abs(vx) < combinedHalfWidths) {
|
||||
if (Math.abs(vy) < combinedHalfHeights) {
|
||||
hit = true;
|
||||
} else {
|
||||
hit = false;
|
||||
}
|
||||
} else {
|
||||
hit = false;
|
||||
}
|
||||
return hit;
|
||||
};
|
||||
|
||||
mixlySprite.repeat = (func) => {
|
||||
mixlySprite.repeatPlay = func;
|
||||
}
|
||||
|
||||
mixlySprite.isKeyboardHit = (keyvalue) => {
|
||||
if (!mixlySprite.keys[keyvalue]) {
|
||||
let key = mixlySprite.keyboard(keyvalue);
|
||||
mixlySprite.keys[keyvalue] = key;
|
||||
}
|
||||
return mixlySprite.keys[keyvalue].isDown;
|
||||
}
|
||||
|
||||
mixlySprite.keyboardListener = (keyvalue, func) => {
|
||||
if (!mixlySprite.keys[keyvalue]) {
|
||||
let key = mixlySprite.keyboard(keyvalue);
|
||||
key.press = function () {
|
||||
if (mixlySprite.state) func();
|
||||
};
|
||||
mixlySprite.keys[keyvalue] = key;
|
||||
} else {
|
||||
mixlySprite.keys[keyvalue].press = function () {
|
||||
if (mixlySprite.state) func();
|
||||
};
|
||||
}
|
||||
}
|
||||
mixlySprite.keyboard = (value) => {
|
||||
let key = {};
|
||||
key.value = value;
|
||||
key.isDown = false;
|
||||
key.isUp = true;
|
||||
key.press = undefined;
|
||||
key.release = undefined;
|
||||
key.downHandler = event => {
|
||||
if (event.key === key.value) {
|
||||
if (key.isUp && key.press) key.press();
|
||||
key.isDown = true;
|
||||
key.isUp = false;
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
key.upHandler = event => {
|
||||
if (event.key === key.value) {
|
||||
if (key.isDown && key.release) key.release();
|
||||
key.isDown = false;
|
||||
key.isUp = true;
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
const downListener = key.downHandler.bind(key);
|
||||
const upListener = key.upHandler.bind(key);
|
||||
window.addEventListener(
|
||||
"keydown", downListener, false
|
||||
);
|
||||
window.addEventListener(
|
||||
"keyup", upListener, false
|
||||
);
|
||||
key.unsubscribe = () => {
|
||||
window.removeEventListener("keydown", downListener);
|
||||
window.removeEventListener("keyup", upListener);
|
||||
};
|
||||
return key;
|
||||
}
|
||||
|
||||
mixlySprite.clearTimer = () => {
|
||||
mixlySprite.startTime = performance.now();
|
||||
}
|
||||
|
||||
mixlySprite.gameLoopDisplay = () => {
|
||||
if (mixlySprite.processingDisplayEvent) {
|
||||
const pSE = mixlySprite.processingDisplayEvent;
|
||||
switch (pSE.displayType) {
|
||||
case 'slide':
|
||||
if (performance.now() >= pSE.totalTime + pSE.startTime) {
|
||||
pSE.sprite.moveTo(pSE.targetX, pSE.targetY);
|
||||
mixlySprite.displayTag = false;
|
||||
mixlySprite.processingDisplayEvent = null;
|
||||
} else {
|
||||
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
|
||||
if (leftLoops >= 1) {
|
||||
pSE.sprite.addX((pSE.targetX - pSE.sprite.x) / leftLoops);
|
||||
pSE.sprite.addY((pSE.targetY - pSE.sprite.y) / leftLoops);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'expand':
|
||||
if (performance.now() >= pSE.totalTime + pSE.startTime) {
|
||||
pSE.sprite.enlargeTo(pSE.targetS);
|
||||
mixlySprite.displayTag = false;
|
||||
mixlySprite.processingDisplayEvent = null;
|
||||
} else {
|
||||
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
|
||||
if (leftLoops >= 1) {
|
||||
pSE.sprite.enlarge((pSE.targetS - Math.sqrt(pSE.sprite.height * pSE.sprite.width)) / leftLoops);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'circle':
|
||||
if (performance.now() >= pSE.totalTime + pSE.startTime) {
|
||||
pSE.sprite.rotateTo(pSE.targetDegree);
|
||||
mixlySprite.displayTag = false;
|
||||
mixlySprite.processingDisplayEvent = null;
|
||||
} else {
|
||||
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
|
||||
if (leftLoops >= 1) {
|
||||
pSE.sprite.rotate((pSE.targetDegree - pSE.sprite.rotation * 180 / Math.PI) / leftLoops);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mixlySprite.running) {
|
||||
if (mixlySprite.successProcessingDisplayEvents.length) {
|
||||
for (var pSEindex = mixlySprite.successProcessingDisplayEvents.length - 1; pSEindex >= 0; pSEindex--) {
|
||||
const pSE = mixlySprite.successProcessingDisplayEvents[pSEindex];
|
||||
switch (pSE.displayType) {
|
||||
case 'slide':
|
||||
if (performance.now() >= pSE.totalTime + pSE.startTime) {
|
||||
pSE.sprite.moveTo(pSE.targetX, pSE.targetY);
|
||||
mixlySprite.successProcessingDisplayEvents.splice(pSEindex, 1);
|
||||
} else {
|
||||
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
|
||||
if (leftLoops >= 1) {
|
||||
pSE.sprite.addX((pSE.targetX - pSE.sprite.x) / leftLoops);
|
||||
pSE.sprite.addY((pSE.targetY - pSE.sprite.y) / leftLoops);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'expand':
|
||||
if (performance.now() >= pSE.totalTime + pSE.startTime) {
|
||||
pSE.sprite.enlargeTo(pSE.targetS);
|
||||
mixlySprite.successProcessingDisplayEvents.splice(pSEindex, 1);
|
||||
} else {
|
||||
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
|
||||
if (leftLoops >= 1) {
|
||||
pSE.sprite.enlarge((pSE.targetS - Math.sqrt(pSE.sprite.height * pSE.sprite.width)) / leftLoops);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'circle':
|
||||
if (performance.now() >= pSE.totalTime + pSE.startTime) {
|
||||
pSE.sprite.rotateTo(pSE.targetDegree);
|
||||
mixlySprite.successProcessingDisplayEvents.splice(pSEindex, 1);
|
||||
} else {
|
||||
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
|
||||
if (leftLoops >= 1) {
|
||||
pSE.sprite.rotate((pSE.targetDegree - pSE.sprite.rotation * 180 / Math.PI) / leftLoops);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mixlySprite.changeWidth = (w) => {
|
||||
const $canvas = $(mixlySprite.renderer.view);
|
||||
const canvasWidth = $canvas.width();
|
||||
const canvasHeight = $canvas.height();
|
||||
$canvas.width(w);
|
||||
$canvas.height(w / canvasWidth * canvasHeight);
|
||||
}
|
||||
|
||||
mixlySprite.kill = () => {
|
||||
mixlySprite.state = false;
|
||||
mixlySprite.repeatPlay = new Function();
|
||||
for (let i in mixlySprite.keys) {
|
||||
mixlySprite.keys[i].unsubscribe();
|
||||
delete mixlySprite.keys[i];
|
||||
}
|
||||
mixlySprite.processingDisplayEvent = null;
|
||||
mixlySprite.displayTag = false;
|
||||
mixlySprite.running = false;
|
||||
mixlySprite.clearTimer();
|
||||
}
|
||||
|
||||
mixlySprite.runit = (container) => {
|
||||
const $container = $(container);
|
||||
$container.empty();
|
||||
// Keep the scale mode to nearest
|
||||
PIXI.SCALE_MODES.DEFAULT = PIXI.SCALE_MODES.NEAREST;
|
||||
mixlySprite.renderer = PIXI.autoDetectRenderer(mixlySprite.canvasWidth, mixlySprite.canvasHeight, { backgroundColor: 0x00FFFFFF });
|
||||
$container.append(mixlySprite.renderer.view);
|
||||
// Create mixlySprite.stage container
|
||||
// mixlySprite.stage = new PIXI.Container();
|
||||
mixlySprite.pointer = { x: 0, y: 0 };
|
||||
mixlySprite.stage.sortableChildren = true;
|
||||
mixlySprite.stage.interactive = true;
|
||||
mixlySprite.stage.on("mousemove", (event = PIXI.InteractionEvent) => {
|
||||
mixlySprite.pointer.x = event.data.global.x;
|
||||
mixlySprite.pointer.y = event.data.global.y;
|
||||
});
|
||||
mixlySprite.lastFrameTime = 0;
|
||||
mixlySprite.running = true;
|
||||
if (!mixlySprite.lastFrameTime) {
|
||||
mixlySprite.lastFrameTime = performance.now();
|
||||
mixlySprite.lastSecond = performance.now();
|
||||
}
|
||||
mixlySprite.animate(performance.now());
|
||||
mixlySprite.repeatPlay = new Function();
|
||||
for (let i in mixlySprite.keys) {
|
||||
mixlySprite.keys[i].unsubscribe();
|
||||
delete mixlySprite.keys[i];
|
||||
}
|
||||
if (mixlySprite.backgroundSprite) {
|
||||
mixlySprite.backgroundSprite.runningMouseDown = new Function();
|
||||
}
|
||||
for (let i in mixlySprite.sprites) {
|
||||
mixlySprite.sprites[i].runningMouseDown = new Function();
|
||||
}
|
||||
for (let i in mixlySprite.texts) {
|
||||
mixlySprite.texts[i].runningMouseDown = new Function();
|
||||
}
|
||||
mixlySprite.processingDisplayEvent = null;
|
||||
mixlySprite.displayTag = false;
|
||||
mixlySprite.clearTimer();
|
||||
mixlySprite.changeWidth($('body').width() / 2);
|
||||
mixlySprite.state = true;
|
||||
}
|
||||
|
||||
window.mixlySprite = mixlySprite;
|
||||
|
||||
export default mixlySprite;
|
||||
274
mixly/boards/default_src/python_pyodide/others/python-shell.js
Normal file
274
mixly/boards/default_src/python_pyodide/others/python-shell.js
Normal file
@@ -0,0 +1,274 @@
|
||||
import * as Blockly from 'blockly/core';
|
||||
import * as path from 'path';
|
||||
import $ from 'jquery';
|
||||
import { createApp } from 'vue';
|
||||
import mixlySprite from './pixi-sprite';
|
||||
import {
|
||||
Workspace,
|
||||
Env,
|
||||
Msg,
|
||||
HTMLTemplate,
|
||||
Debug,
|
||||
app
|
||||
} from 'mixly';
|
||||
import { KernelLoader } from '@basthon/kernel-loader';
|
||||
import StatusBarImage from './statusbar-image';
|
||||
import StatusBarFileSystem from './statusbar-filesystem';
|
||||
import StatusBarTool from './statusbar-tool';
|
||||
import StatusBarGame from './statusbar-game';
|
||||
import TeachableMachineApp from './teachableMachine/App.vue';
|
||||
import LOADER_TEMPLATE from '../templates/html/loader.html';
|
||||
|
||||
import 'element-plus/dist/index.css';
|
||||
|
||||
|
||||
export default class PythonShell {
|
||||
static {
|
||||
HTMLTemplate.add(
|
||||
'html/statusbar/loader.html',
|
||||
new HTMLTemplate(LOADER_TEMPLATE)
|
||||
);
|
||||
|
||||
this.pythonShell = null;
|
||||
this.kernelLoaded = false;
|
||||
this.$loader = $(HTMLTemplate.get('html/statusbar/loader.html').render({
|
||||
msg: {
|
||||
loading: Blockly.Msg.PYTHON_PYODIDE_LOADING
|
||||
}
|
||||
}));
|
||||
this.statusBarImage = null;
|
||||
this.statusBarFileSystem = null;
|
||||
|
||||
this.init = async function () {
|
||||
const footerBar = app.getFooterBar();
|
||||
const $content = footerBar.getContent();
|
||||
$content.after(this.$loader);
|
||||
|
||||
const projectPath = path.relative(Env.indexDirPath, Env.boardDirPath);
|
||||
const loader = new KernelLoader({
|
||||
rootPath: path.join(projectPath, 'deps'),
|
||||
language: 'python3',
|
||||
});
|
||||
|
||||
const kernel = await loader.kernelAvailable();
|
||||
if (!kernel) {
|
||||
return;
|
||||
}
|
||||
await kernel.init();
|
||||
await kernel.loaded();
|
||||
|
||||
this.loader = loader;
|
||||
this.kernel = kernel;
|
||||
this.statusBarImage = StatusBarImage.init();
|
||||
this.statusBarFileSystem = StatusBarFileSystem.init();
|
||||
this.statusBarTool = StatusBarTool.init();
|
||||
const teachableMachineApp = createApp(TeachableMachineApp);
|
||||
teachableMachineApp.mount(this.statusBarTool.getContent()[0]);
|
||||
this.statusBarGame = StatusBarGame.init();
|
||||
this.pythonShell = new PythonShell();
|
||||
this.pyodide = window.pyodide;
|
||||
this.interruptBuffer = new Uint8Array(new ArrayBuffer(1));
|
||||
this.pyodide.setInterruptBuffer(this.interruptBuffer);
|
||||
this.kernelLoaded = true;
|
||||
this.$loader.remove();
|
||||
if (this.$loader && this.$loader.remove) {
|
||||
this.$loader.remove();
|
||||
}
|
||||
this.$loader = null;
|
||||
}
|
||||
|
||||
this.run = async function () {
|
||||
if (!this.kernelLoaded) {
|
||||
return;
|
||||
}
|
||||
const mainWorkspace = Workspace.getMain();
|
||||
const editor = mainWorkspace.getEditorsManager().getActive();
|
||||
const code = editor.getCode();
|
||||
return this.pythonShell.run(code);
|
||||
}
|
||||
|
||||
this.stop = async function () {
|
||||
if (!this.kernelLoaded) {
|
||||
return;
|
||||
}
|
||||
return this.pythonShell.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#statusBarTerminal_ = null;
|
||||
#statusBarImage_ = null;
|
||||
#statusBarsManager_ = null;
|
||||
#cursor_ = {
|
||||
row: 0,
|
||||
column: 0
|
||||
};
|
||||
#prompt_ = '';
|
||||
#inputResolve_ = null;
|
||||
#inputReject_ = null;
|
||||
#waittingForInput_ = false;
|
||||
#running_ = false;
|
||||
#kernel_ = null;
|
||||
#onCursorChangeEvent_ = () => this.#onCursorChange_();
|
||||
#commands_ = [
|
||||
{
|
||||
name: 'REPL-Enter',
|
||||
bindKey: 'Enter',
|
||||
exec: (editor) => {
|
||||
const session = editor.getSession();
|
||||
const cursor = session.selection.getCursor();
|
||||
if (cursor.row === this.#cursor_.row) {
|
||||
const newPos = this.#statusBarTerminal_.getEndPos();
|
||||
let str = this.#statusBarTerminal_.getValueRange(this.#cursor_, newPos);
|
||||
str = str.replace(this.#prompt_, '');
|
||||
this.#inputResolve_?.(str);
|
||||
this.#inputResolve_ = null;
|
||||
this.#inputReject_ = null;
|
||||
this.#statusBarTerminal_.addValue('\n');
|
||||
this.#exitInput_();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, {
|
||||
name: 'REPL-ChangeEditor',
|
||||
bindKey: 'Delete|Ctrl-X|Backspace',
|
||||
exec: (editor) => {
|
||||
const session = editor.getSession();
|
||||
const cursor = session.selection.getCursor();
|
||||
if (cursor.row < this.#cursor_.row || cursor.column <= this.#cursor_.column) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
constructor() {
|
||||
const mainWorkspace = Workspace.getMain();
|
||||
this.#statusBarsManager_ = mainWorkspace.getStatusBarsManager();
|
||||
this.#statusBarTerminal_ = this.#statusBarsManager_.getStatusBarById('output');
|
||||
this.#statusBarImage_ = this.#statusBarsManager_.getStatusBarById('images');
|
||||
this.#kernel_ = PythonShell.kernel;
|
||||
this.#addEventsListener_();
|
||||
}
|
||||
|
||||
#addEventsListener_() {
|
||||
this.#kernel_.addEventListener('eval.finished', () => {
|
||||
this.#running_ = false;
|
||||
this.#statusBarTerminal_.addValue(`\n==${Msg.Lang['shell.finish']}==`);
|
||||
this.syncfs(false).catch(Debug.error);
|
||||
});
|
||||
|
||||
this.#kernel_.addEventListener('eval.output', (data) => {
|
||||
this.#statusBarTerminal_.addValue(data.content);
|
||||
});
|
||||
|
||||
this.#kernel_.addEventListener('eval.error', () => {
|
||||
this.#running_ = false;
|
||||
this.#statusBarTerminal_.addValue(`\n==${Msg.Lang['shell.finish']}==`);
|
||||
});
|
||||
|
||||
this.#kernel_.addEventListener('eval.input', (data) => {
|
||||
const prompt = String(data?.content?.prompt);
|
||||
this.#statusBarTerminal_.addValue(prompt);
|
||||
this.#prompt_ = prompt;
|
||||
this.#inputResolve_ = data.resolve;
|
||||
this.#inputReject_ = data.reject;
|
||||
this.#enterInput_();
|
||||
});
|
||||
|
||||
this.#kernel_.addEventListener('eval.display', (data) => {
|
||||
this.#statusBarsManager_.changeTo('images');
|
||||
this.#statusBarImage_.display(data);
|
||||
});
|
||||
}
|
||||
|
||||
#onCursorChange_() {
|
||||
const editor = this.#statusBarTerminal_.getEditor();
|
||||
const session = editor.getSession();
|
||||
const cursor = session.selection.getCursor();
|
||||
editor.setReadOnly(
|
||||
cursor.row < this.#cursor_.row || cursor.column < this.#cursor_.column
|
||||
);
|
||||
}
|
||||
|
||||
#enterInput_() {
|
||||
if (!this.#running_) {
|
||||
return;
|
||||
}
|
||||
this.#waittingForInput_ = true;
|
||||
this.#cursor_ = this.#statusBarTerminal_.getEndPos();
|
||||
const editor = this.#statusBarTerminal_.getEditor();
|
||||
editor.setReadOnly(false);
|
||||
editor.focus();
|
||||
const session = editor.getSession();
|
||||
session.selection.on('changeCursor', this.#onCursorChangeEvent_);
|
||||
editor.commands.addCommands(this.#commands_);
|
||||
}
|
||||
|
||||
#exitInput_() {
|
||||
this.#waittingForInput_ = false;
|
||||
const editor = this.#statusBarTerminal_.getEditor();
|
||||
const session = editor.getSession();
|
||||
session.selection.off('changeCursor', this.#onCursorChangeEvent_);
|
||||
editor.commands.removeCommands(this.#commands_);
|
||||
this.#prompt_ = '';
|
||||
this.#inputResolve_?.('');
|
||||
// this.#inputReject_?.({});
|
||||
this.cursor_ = { row: 0, column: 0 };
|
||||
editor.setReadOnly(true);
|
||||
}
|
||||
|
||||
async run(code) {
|
||||
await this.stop();
|
||||
await this.syncfs(true);
|
||||
if (code.indexOf('import turtle') !== -1) {
|
||||
code += '\nturtle.done()\n';
|
||||
}
|
||||
if (code.indexOf('import matplotlib.pyplot') !== -1) {
|
||||
code += '\nplt.clf()\n';
|
||||
}
|
||||
mixlySprite.runit(this.#statusBarImage_.getContent()[0]);
|
||||
if (code.indexOf('import sprite') !== -1 || code.indexOf('from sprite import') !== -1) {
|
||||
this.#statusBarsManager_.changeTo('images');
|
||||
} else {
|
||||
this.#statusBarsManager_.changeTo('output');
|
||||
}
|
||||
this.#statusBarsManager_.show();
|
||||
this.#statusBarTerminal_.setValue(`${Msg.Lang['shell.running']}...\n`);
|
||||
this.#running_ = true;
|
||||
this.#kernel_.dispatchEvent('eval.request', {
|
||||
code,
|
||||
interactive: false,
|
||||
});
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.#waittingForInput_) {
|
||||
this.#exitInput_();
|
||||
}
|
||||
if (this.#running_) {
|
||||
const timeout = 5;
|
||||
PythonShell.interruptBuffer[0] = 2;
|
||||
const startTime = Number(new Date());
|
||||
while (Number(new Date()) - startTime < timeout * 1000) {
|
||||
if (this.#running_) {
|
||||
PythonShell.interruptBuffer[0] = 2;
|
||||
await this.sleep(100);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.#running_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
async syncfs(populate = false) {
|
||||
return new Promise((resolve) => {
|
||||
window.pyodide.FS.syncfs(populate, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
1825
mixly/boards/default_src/python_pyodide/others/sound.js
Normal file
1825
mixly/boards/default_src/python_pyodide/others/sound.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,496 @@
|
||||
import * as Blockly from 'blockly/core';
|
||||
import { layer } from 'layui';
|
||||
import * as path from 'path';
|
||||
import $ from 'jquery';
|
||||
import {
|
||||
PageBase,
|
||||
Msg,
|
||||
HTMLTemplate,
|
||||
DragV,
|
||||
StatusBar,
|
||||
Menu,
|
||||
Debug,
|
||||
StatusBarsManager,
|
||||
Workspace
|
||||
} from 'mixly';
|
||||
import FileSystemFileTree from './filesystem-file-tree';
|
||||
import FILE_SYSTEM_TEMPLATE from '../templates/html/statusbar-filesystem.html';
|
||||
import FILE_SYSTEM_OPEN_FS_TEMPLATE from '../templates/html/statusbar-filesystem-open-fs.html';
|
||||
import FILE_SYSTEM_EDITOR_EMPTY_TEMPLATE from '../templates/html/statusbar-filesystem-editor-empty.html';
|
||||
|
||||
|
||||
|
||||
export default class StatusBarFileSystem extends PageBase {
|
||||
static {
|
||||
HTMLTemplate.add(
|
||||
'html/statusbar/statusbar-filesystem.html',
|
||||
new HTMLTemplate(FILE_SYSTEM_TEMPLATE)
|
||||
);
|
||||
|
||||
HTMLTemplate.add(
|
||||
'html/statusbar/statusbar-filesystem-open-fs.html',
|
||||
new HTMLTemplate(FILE_SYSTEM_OPEN_FS_TEMPLATE)
|
||||
);
|
||||
|
||||
HTMLTemplate.add(
|
||||
'html/statusbar/statusbar-filesystem-editor-empty.html',
|
||||
new HTMLTemplate(FILE_SYSTEM_EDITOR_EMPTY_TEMPLATE)
|
||||
);
|
||||
|
||||
this.init = function () {
|
||||
StatusBarsManager.typesRegistry.register(['file-system'], StatusBarFileSystem);
|
||||
const mainWorkspace = Workspace.getMain();
|
||||
const statusBarsManager = mainWorkspace.getStatusBarsManager();
|
||||
statusBarsManager.add({
|
||||
type: 'file-system',
|
||||
id: 'file-system',
|
||||
name: Blockly.Msg.PYTHON_PYODIDE_FILE_SYSTEM,
|
||||
title: Blockly.Msg.PYTHON_PYODIDE_FILE_SYSTEM
|
||||
});
|
||||
statusBarsManager.changeTo('output');
|
||||
return statusBarsManager.get('file-system');
|
||||
}
|
||||
}
|
||||
|
||||
#$close_ = null;
|
||||
#$fileTree_ = null;
|
||||
#$editor_ = null;
|
||||
#$openFS_ = null;
|
||||
#$editorEmpty_ = null;
|
||||
#editor_ = null;
|
||||
#fileTree_ = null;
|
||||
#drag_ = null;
|
||||
#fileTreeShown_ = false;
|
||||
#editorShown_ = false;
|
||||
#changed_ = false;
|
||||
#nativefs_ = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-filesystem.html').render());
|
||||
this.setContent($content);
|
||||
this.#fileTree_ = new FileSystemFileTree();
|
||||
this.#$fileTree_ = $content.children('.file-tree');
|
||||
this.#$openFS_ = $(HTMLTemplate.get('html/statusbar/statusbar-filesystem-open-fs.html').render({
|
||||
msg: {
|
||||
loadFS: Blockly.Msg.PYTHON_PYODIDE_LOAD_FILE_SYSTEM
|
||||
}
|
||||
}));
|
||||
this.#$fileTree_.append(this.#$openFS_);
|
||||
this.#editor_ = new StatusBar();
|
||||
this.#$editor_ = $content.children('.editor');
|
||||
this.#$editorEmpty_ = $(HTMLTemplate.get('html/statusbar/statusbar-filesystem-editor-empty.html').render());
|
||||
this.#$editor_.append(this.#$editorEmpty_);
|
||||
}
|
||||
|
||||
#addEventsListener_() {
|
||||
this.#drag_ = new DragV(this.getContent()[0], {
|
||||
min: '150px',
|
||||
startSize: '15%',
|
||||
full: [false, false]
|
||||
});
|
||||
|
||||
this.#drag_.bind('sizeChanged', () => {
|
||||
this.resize();
|
||||
});
|
||||
|
||||
this.#$openFS_.children('button').click(() => {
|
||||
this.selectFS().catch(Debug.error);
|
||||
});
|
||||
|
||||
this.#fileTree_.bind('beforeSelectLeaf', (selected) => {
|
||||
const filePath = selected[0].id;
|
||||
const mode = this.#editor_.getFileMode(path.extname(filePath));
|
||||
if (!mode) {
|
||||
layer.msg(Msg.Lang['statusbar.ampy.cannotEdit'], { time: 1000 });
|
||||
return false;
|
||||
}
|
||||
this.#editor_.setMode(mode);
|
||||
return true;
|
||||
});
|
||||
|
||||
this.#fileTree_.bind('afterSelectLeaf', async (selected) => {
|
||||
const filePath = selected[0].id;
|
||||
this.#fileTree_.showProgress();
|
||||
const fs = this.#fileTree_.getFS();
|
||||
try {
|
||||
const result = await fs.readFile(filePath);
|
||||
this.showEditor();
|
||||
this.#editor_.setValue(result);
|
||||
this.#editor_.scrollToTop();
|
||||
this.#editor_.focus();
|
||||
this.setStatus(false);
|
||||
} catch (error) {
|
||||
Debug.error(error);
|
||||
this.hideEditor();
|
||||
this.#fileTree_.deselectAll();
|
||||
}
|
||||
this.#fileTree_.hideProgress();
|
||||
});
|
||||
|
||||
this.#fileTree_.bind('afterCreateNode', (folderPath) => {
|
||||
this.#fileTree_.refreshFolder(folderPath);
|
||||
});
|
||||
|
||||
this.#fileTree_.bind('afterDeleteNode', (folderPath) => {
|
||||
this.#fileTree_.refreshFolder(folderPath);
|
||||
});
|
||||
|
||||
this.#fileTree_.bind('afterRenameNode', (folderPath) => {
|
||||
this.#fileTree_.refreshFolder(folderPath);
|
||||
});
|
||||
|
||||
this.#fileTree_.bind('afterRefreshNode', () => {
|
||||
const selectedNodeId = this.#fileTree_.getSelectedNodeId();
|
||||
if (!selectedNodeId) {
|
||||
this.hideEditor();
|
||||
}
|
||||
});
|
||||
|
||||
const fileTreeContextMenu = this.#fileTree_.getContextMenu();
|
||||
const fileTreeMenu = fileTreeContextMenu.getItem('menu');
|
||||
|
||||
fileTreeMenu.add({
|
||||
weight: 7,
|
||||
id: 'copy_path',
|
||||
data: {
|
||||
isHtmlName: true,
|
||||
name: Menu.getItem(Msg.Lang['fileTree.copyPath']),
|
||||
callback: (_, { $trigger }) => {
|
||||
let outPath = null;
|
||||
let type = $trigger.attr('type');
|
||||
if (type === 'root') {
|
||||
outPath = this.#fileTree_.getRootFolderTitle();
|
||||
} else {
|
||||
outPath = $trigger.attr('title');
|
||||
}
|
||||
navigator.clipboard.writeText(outPath).catch(Debug.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileTreeMenu.add({
|
||||
weight: 14,
|
||||
id: 'sep5',
|
||||
preconditionFn: ($trigger) => {
|
||||
const selectedNodeId = this.#fileTree_.getSelectedNodeId();
|
||||
let type = $trigger.attr('type');
|
||||
let id = $trigger.attr('id');
|
||||
if (type === 'file' && selectedNodeId !== id) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
data: '---------'
|
||||
});
|
||||
|
||||
fileTreeMenu.add({
|
||||
weight: 15,
|
||||
id: 'refresh',
|
||||
preconditionFn: ($trigger) => {
|
||||
const selectedNodeId = this.#fileTree_.getSelectedNodeId();
|
||||
let type = $trigger.attr('type');
|
||||
let id = $trigger.attr('id');
|
||||
if (type === 'file' && selectedNodeId !== id) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
data: {
|
||||
isHtmlName: true,
|
||||
name: Menu.getItem(Msg.Lang['statusbar.ampy.refresh']),
|
||||
callback: (_, { $trigger }) => {
|
||||
let type = $trigger.attr('type');
|
||||
if (type === 'root') {
|
||||
this.#fileTree_.openRootFolder();
|
||||
this.#fileTree_.refreshFolder('/');
|
||||
} else if (type === 'folder') {
|
||||
let id = $trigger.attr('id');
|
||||
this.#fileTree_.openNode(id);
|
||||
this.#fileTree_.refreshFolder(id);
|
||||
} else {
|
||||
const nodes = this.#fileTree_.getSelectedNodes();
|
||||
this.#fileTree_.runEvent('afterSelectLeaf', nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileTreeMenu.add({
|
||||
weight: 16,
|
||||
id: 'sep6',
|
||||
preconditionFn: ($trigger) => {
|
||||
let type = $trigger.attr('type');
|
||||
return ['root'].includes(type);
|
||||
},
|
||||
data: '---------'
|
||||
});
|
||||
|
||||
fileTreeMenu.add({
|
||||
weight: 17,
|
||||
id: 'exit',
|
||||
preconditionFn: ($trigger) => {
|
||||
let type = $trigger.attr('type');
|
||||
return ['root'].includes(type);
|
||||
},
|
||||
data: {
|
||||
isHtmlName: true,
|
||||
name: Menu.getItem(Msg.Lang['statusbar.ampy.exit']),
|
||||
callback: () => {
|
||||
this.closeFS();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileTreeMenu.remove('copy');
|
||||
fileTreeMenu.remove('cut');
|
||||
fileTreeMenu.remove('paste');
|
||||
fileTreeMenu.remove('sep2');
|
||||
|
||||
const editorContextMenu = this.#editor_.getContextMenu();
|
||||
const editorMenu = editorContextMenu.getItem('code');
|
||||
|
||||
editorMenu.empty();
|
||||
|
||||
editorMenu.add({
|
||||
weight: 0,
|
||||
id: 'cut',
|
||||
data: {
|
||||
isHtmlName: true,
|
||||
name: Menu.getItem(Msg.Lang['editor.contextMenu.cut'], 'Ctrl+X'),
|
||||
callback: () => this.#editor_.cut()
|
||||
}
|
||||
});
|
||||
editorMenu.add({
|
||||
weight: 1,
|
||||
id: 'copy',
|
||||
data: {
|
||||
isHtmlName: true,
|
||||
name: Menu.getItem(Msg.Lang['editor.contextMenu.copy'], 'Ctrl+C'),
|
||||
callback: () => this.#editor_.copy()
|
||||
}
|
||||
});
|
||||
editorMenu.add({
|
||||
weight: 2,
|
||||
id: 'paste',
|
||||
data: {
|
||||
isHtmlName: true,
|
||||
name: Menu.getItem(Msg.Lang['editor.contextMenu.paste'], 'Ctrl+V'),
|
||||
callback: () => this.#editor_.paste()
|
||||
}
|
||||
});
|
||||
editorMenu.add({
|
||||
weight: 3,
|
||||
id: 'sep1',
|
||||
data: '---------'
|
||||
});
|
||||
editorMenu.add({
|
||||
weight: 4,
|
||||
id: 'togglecomment',
|
||||
data: {
|
||||
isHtmlName: true,
|
||||
name: Menu.getItem(Msg.Lang['editor.contextMenu.togglecomment'], 'Ctrl+/'),
|
||||
callback: () => this.#editor_.commentLine()
|
||||
}
|
||||
});
|
||||
|
||||
editorMenu.add({
|
||||
weight: 6,
|
||||
id: 'sep2',
|
||||
preconditionFn: () => {
|
||||
return this.#changed_;
|
||||
},
|
||||
data: '---------'
|
||||
});
|
||||
|
||||
editorMenu.add({
|
||||
weight: 7,
|
||||
id: 'save',
|
||||
preconditionFn: () => {
|
||||
return this.#changed_;
|
||||
},
|
||||
data: {
|
||||
isHtmlName: true,
|
||||
name: Menu.getItem(Msg.Lang['file.save'], 'Ctrl+S'),
|
||||
callback: async () => {
|
||||
await this.put();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { commands } = this.#editor_.getEditor();
|
||||
commands.addCommand({
|
||||
name: "save",
|
||||
bindKey: "Ctrl-S",
|
||||
exec: async () => {
|
||||
if (!this.#changed_) {
|
||||
return;
|
||||
}
|
||||
await this.put();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async put() {
|
||||
this.#fileTree_.showProgress();
|
||||
const id = this.#fileTree_.getSelectedNodeId();
|
||||
const fs = this.#fileTree_.getFS();
|
||||
try {
|
||||
await fs.writeFile(id, this.#editor_.getValue());
|
||||
this.setStatus(false);
|
||||
} catch (error) {
|
||||
Debug.error(error);
|
||||
} finally {
|
||||
this.#fileTree_.hideProgress();
|
||||
}
|
||||
}
|
||||
|
||||
showFileTree() {
|
||||
if (this.#fileTreeShown_) {
|
||||
return;
|
||||
}
|
||||
this.#$openFS_.detach();
|
||||
this.#$fileTree_.empty();
|
||||
this.#$fileTree_.append(this.#fileTree_.getContent());
|
||||
this.#fileTreeShown_ = true;
|
||||
}
|
||||
|
||||
hideFileTree() {
|
||||
if (!this.#fileTreeShown_) {
|
||||
return;
|
||||
}
|
||||
this.#fileTree_.getContent().detach();
|
||||
this.#$fileTree_.empty();
|
||||
this.#$fileTree_.append(this.#$openFS_);
|
||||
this.#fileTreeShown_ = false;
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
if (this.#editorShown_) {
|
||||
return;
|
||||
}
|
||||
this.#$editorEmpty_.detach();
|
||||
this.#$editor_.empty();
|
||||
this.#$editor_.append(this.#editor_.getContent());
|
||||
this.#editorShown_ = true;
|
||||
}
|
||||
|
||||
hideEditor() {
|
||||
if (!this.#editorShown_) {
|
||||
return;
|
||||
}
|
||||
this.#editor_.getContent().detach();
|
||||
this.#$editor_.empty();
|
||||
this.#$editor_.append(this.#$editorEmpty_);
|
||||
this.#editorShown_ = false;
|
||||
this.setStatus(false);
|
||||
}
|
||||
|
||||
getDrag() {
|
||||
return this.#drag_;
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.hideCloseBtn();
|
||||
this.#editor_.init();
|
||||
this.#addEventsListener_();
|
||||
const editor = this.#editor_.getEditor();
|
||||
editor.setReadOnly(false);
|
||||
editor.renderer.setShowGutter(true);
|
||||
editor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: true,
|
||||
newLineMode: 'unix'
|
||||
});
|
||||
editor.on('change', () => {
|
||||
this.setStatus(true);
|
||||
});
|
||||
this.loadFS().catch(Debug.error);
|
||||
}
|
||||
|
||||
async loadFS() {
|
||||
const fs = this.#fileTree_.getFS();
|
||||
const directoryHandle = await fs.loadFS();
|
||||
await this.openFS(directoryHandle);
|
||||
}
|
||||
|
||||
async selectFS() {
|
||||
const fs = this.#fileTree_.getFS();
|
||||
const directoryHandle = await fs.showDirectoryPicker();
|
||||
await this.openFS(directoryHandle);
|
||||
}
|
||||
|
||||
async openFS(directoryHandle) {
|
||||
if (!directoryHandle?.name) {
|
||||
return;
|
||||
}
|
||||
const rootPath = '/' + directoryHandle.name;
|
||||
this.#fileTree_.setFolderPath('/');
|
||||
this.#fileTree_.setRootFolderTitle(rootPath);
|
||||
this.#fileTree_.setRootFolderName(directoryHandle.name);
|
||||
this.#fileTree_.openRootFolder();
|
||||
this.showFileTree();
|
||||
this.#nativefs_ = await window.pyodide.mountNativeFS(rootPath, directoryHandle);
|
||||
}
|
||||
|
||||
closeFS() {
|
||||
const rootPath = this.#fileTree_.getRootFolderTitle();
|
||||
const rootContents = Object.keys(window.pyodide.FS.root.contents);
|
||||
if (rootContents.includes(path.basename(rootPath))) {
|
||||
const lookup = window.pyodide.FS.lookupPath(rootPath, {
|
||||
follow_mount: false
|
||||
});
|
||||
if (window.pyodide.FS.isMountpoint(lookup.node)) {
|
||||
window.pyodide.FS.unmount(rootPath);
|
||||
}
|
||||
}
|
||||
const fs = this.#fileTree_.getFS();
|
||||
fs.setFSCache(null);
|
||||
this.#fileTree_.deselectAll();
|
||||
this.hideFileTree();
|
||||
this.hideEditor();
|
||||
this.setStatus(false);
|
||||
}
|
||||
|
||||
onMounted() {
|
||||
super.onMounted();
|
||||
this.#editor_.onMounted();
|
||||
this.#fileTree_.onMounted();
|
||||
// this.#fileTree_.refreshFolder('/');
|
||||
}
|
||||
|
||||
onUnmounted() {
|
||||
// this.hideEditor();
|
||||
// this.#fileTree_.deselectAll();
|
||||
super.onUnmounted();
|
||||
this.#editor_.onUnmounted();
|
||||
this.#fileTree_.onUnmounted();
|
||||
}
|
||||
|
||||
resize() {
|
||||
super.resize();
|
||||
this.#editor_.resize();
|
||||
this.#fileTree_.resize();
|
||||
}
|
||||
|
||||
setStatus(isChanged) {
|
||||
if (this.#changed_ === isChanged) {
|
||||
return;
|
||||
}
|
||||
this.#changed_ = isChanged;
|
||||
}
|
||||
|
||||
getNativeFS() {
|
||||
return this.#nativefs_;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.#editor_.dispose();
|
||||
this.#editor_ = null;
|
||||
this.#fileTree_.dispose();
|
||||
this.#fileTree_ = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
227
mixly/boards/default_src/python_pyodide/others/statusbar-game.js
Normal file
227
mixly/boards/default_src/python_pyodide/others/statusbar-game.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import $ from 'jquery';
|
||||
import { Msg } from 'blockly/core';
|
||||
import {
|
||||
PageBase,
|
||||
HTMLTemplate,
|
||||
StatusBarsManager,
|
||||
Workspace
|
||||
} from 'mixly';
|
||||
import '../language/loader';
|
||||
import STATUS_BAR_GAME_TEMPLATE from '../templates/html/statusbar-game.html';
|
||||
|
||||
|
||||
export default class StatusBarGame extends PageBase {
|
||||
static {
|
||||
HTMLTemplate.add(
|
||||
'html/statusbar/statusbar-game.html',
|
||||
new HTMLTemplate(STATUS_BAR_GAME_TEMPLATE)
|
||||
);
|
||||
|
||||
this.init = function () {
|
||||
StatusBarsManager.typesRegistry.register(['game'], StatusBarGame);
|
||||
const mainWorkspace = Workspace.getMain();
|
||||
const statusBarsManager = mainWorkspace.getStatusBarsManager();
|
||||
statusBarsManager.add({
|
||||
type: 'game',
|
||||
id: 'game',
|
||||
name: Msg.PYTHON_PYODIDE_GAME,
|
||||
title: Msg.PYTHON_PYODIDE_GAME
|
||||
});
|
||||
statusBarsManager.changeTo('output');
|
||||
return statusBarsManager.get('game');
|
||||
}
|
||||
}
|
||||
|
||||
#$startBtn_ = null;
|
||||
#$pauseBtn_ = null;
|
||||
#$randomBtn_ = null;
|
||||
#$resetBtn_ = null;
|
||||
#$generation_ = null;
|
||||
#$grid_ = null;
|
||||
#GRID_SIZE_ = 10;
|
||||
#SPEED_ = 500;
|
||||
#grid_ = [];
|
||||
#isRunning_ = false;
|
||||
#generation_ = 0;
|
||||
#intervalId_ = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-game.html').render({
|
||||
epoch: Msg.PYTHON_PYODIDE_GAME_EPOCH,
|
||||
start: Msg.PYTHON_PYODIDE_GAME_START,
|
||||
pause: Msg.PYTHON_PYODIDE_GAME_PAUSE,
|
||||
random: Msg.PYTHON_PYODIDE_GAME_RANDOM,
|
||||
reset: Msg.PYTHON_PYODIDE_GAME_RESET
|
||||
}));
|
||||
this.setContent($content);
|
||||
this.#$startBtn_ = $content.find('.start-btn');
|
||||
this.#$pauseBtn_ = $content.find('.pause-btn');
|
||||
this.#$randomBtn_ = $content.find('.random-btn');
|
||||
this.#$resetBtn_ = $content.find('.reset-btn');
|
||||
this.#$generation_ = $content.find('.generation');
|
||||
this.#$grid_ = $content.find('.grid');
|
||||
this.#addEventListeners_();
|
||||
}
|
||||
|
||||
#addEventListeners_() {
|
||||
this.#$startBtn_.click(() => this.startGame());
|
||||
this.#$pauseBtn_.click(() => this.pauseGame());
|
||||
this.#$randomBtn_.click(() => this.randomInitialize());
|
||||
this.#$resetBtn_.click(() => this.resetGame());
|
||||
}
|
||||
|
||||
// 初始化网格
|
||||
initializeGrid() {
|
||||
this.#$grid_.empty();
|
||||
this.#grid_ = [];
|
||||
|
||||
for (let i = 0; i < this.#GRID_SIZE_; i++) {
|
||||
this.#grid_[i] = [];
|
||||
for (let j = 0; j < this.#GRID_SIZE_; j++) {
|
||||
this.#grid_[i][j] = 0; // 0表示死亡,1表示存活
|
||||
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'cell';
|
||||
cell.dataset.row = i;
|
||||
cell.dataset.col = j;
|
||||
|
||||
cell.addEventListener('click', () => this.toggleCell(i, j));
|
||||
|
||||
this.#$grid_.append(cell);
|
||||
}
|
||||
}
|
||||
this.updateGridDisplay();
|
||||
}
|
||||
|
||||
// 切换细胞状态
|
||||
toggleCell(row, col) {
|
||||
if (!this.#isRunning_) {
|
||||
this.#grid_[row][col] = this.#grid_[row][col] === 0 ? 1 : 0;
|
||||
this.updateGridDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新网格显示
|
||||
updateGridDisplay() {
|
||||
const $cells = this.#$grid_.children('.cell');
|
||||
for (let i = 0; i < $cells.length; i++) {
|
||||
const cell = $cells[i];
|
||||
const row = parseInt(cell.dataset.row);
|
||||
const col = parseInt(cell.dataset.col);
|
||||
if (this.#grid_[row][col] === 1) {
|
||||
cell.classList.add('alive');
|
||||
} else {
|
||||
cell.classList.remove('alive');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算下一代
|
||||
nextGeneration() {
|
||||
const newGrid = [];
|
||||
|
||||
for (let i = 0; i < this.#GRID_SIZE_; i++) {
|
||||
newGrid[i] = [];
|
||||
for (let j = 0; j < this.#GRID_SIZE_; j++) {
|
||||
const neighbors = this.countNeighbors(i, j);
|
||||
|
||||
if (this.#grid_[i][j] === 1) {
|
||||
// 存活细胞:周围有2-3个存活细胞则继续存活
|
||||
newGrid[i][j] = (neighbors === 2 || neighbors === 3) ? 1 : 0;
|
||||
} else {
|
||||
// 死亡细胞:周围有3个存活细胞则复活
|
||||
newGrid[i][j] = neighbors === 3 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.#grid_ = newGrid;
|
||||
this.#generation_++;
|
||||
this.#$generation_.text(this.#generation_);
|
||||
this.updateGridDisplay();
|
||||
}
|
||||
|
||||
// 计算周围存活细胞数量
|
||||
countNeighbors(row, col) {
|
||||
let count = 0;
|
||||
for (let i = -1; i <= 1; i++) {
|
||||
for (let j = -1; j <= 1; j++) {
|
||||
if (i === 0 && j === 0) continue; // 跳过自身
|
||||
|
||||
const newRow = row + i;
|
||||
const newCol = col + j;
|
||||
|
||||
// 检查边界
|
||||
if (newRow >= 0 && newRow < this.#GRID_SIZE_ && newCol >= 0 && newCol < this.#GRID_SIZE_) {
|
||||
count += this.#grid_[newRow][newCol];
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
startGame() {
|
||||
if (!this.#isRunning_) {
|
||||
this.#isRunning_ = true;
|
||||
this.#generation_ = 0;
|
||||
this.#$generation_.text(this.#generation_);
|
||||
this.#intervalId_ = setInterval(() => this.nextGeneration(), this.#SPEED_);
|
||||
this.updateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停游戏
|
||||
pauseGame() {
|
||||
if (this.#isRunning_) {
|
||||
this.#isRunning_ = false;
|
||||
clearInterval(this.#intervalId_);
|
||||
this.updateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// 随机初始化网格
|
||||
randomInitialize() {
|
||||
if (!this.#isRunning_) {
|
||||
for (let i = 0; i < this.#GRID_SIZE_; i++) {
|
||||
for (let j = 0; j < this.#GRID_SIZE_; j++) {
|
||||
// 25%的概率生成存活细胞
|
||||
this.#grid_[i][j] = Math.random() < 0.25 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
this.updateGridDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
// 重置游戏
|
||||
resetGame() {
|
||||
this.#isRunning_ = false;
|
||||
clearInterval(this.#intervalId_);
|
||||
this.#generation_ = 0;
|
||||
this.#$generation_.text(this.#generation_);
|
||||
this.initializeGrid();
|
||||
this.updateButtons();
|
||||
}
|
||||
|
||||
// 更新按钮状态
|
||||
updateButtons() {
|
||||
this.#$startBtn_.attr('disabled', this.#isRunning_);
|
||||
this.#$pauseBtn_.attr('disabled', !this.#isRunning_);
|
||||
this.#$randomBtn_.attr('disabled', this.#isRunning_);
|
||||
this.#$resetBtn_.attr('disabled', false);
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.hideCloseBtn();
|
||||
this.initializeGrid();
|
||||
this.updateButtons();
|
||||
}
|
||||
|
||||
onMounted() { }
|
||||
|
||||
onUnmounted() { }
|
||||
|
||||
resize() { }
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import $ from 'jquery';
|
||||
import { Msg } from 'blockly/core';
|
||||
import {
|
||||
PageBase,
|
||||
HTMLTemplate,
|
||||
StatusBarsManager,
|
||||
Workspace
|
||||
} from 'mixly';
|
||||
import '../language/loader';
|
||||
import STATUS_BAR_IMAGE_TEMPLATE from '../templates/html/statusbar-image.html';
|
||||
|
||||
|
||||
export default class StatusBarImage extends PageBase {
|
||||
static {
|
||||
HTMLTemplate.add(
|
||||
'html/statusbar/statusbar-image.html',
|
||||
new HTMLTemplate(STATUS_BAR_IMAGE_TEMPLATE)
|
||||
);
|
||||
|
||||
this.init = function () {
|
||||
StatusBarsManager.typesRegistry.register(['images'], StatusBarImage);
|
||||
const mainWorkspace = Workspace.getMain();
|
||||
const statusBarsManager = mainWorkspace.getStatusBarsManager();
|
||||
statusBarsManager.add({
|
||||
type: 'images',
|
||||
id: 'images',
|
||||
name: Msg.PYTHON_PYODIDE_IMAGE,
|
||||
title: Msg.PYTHON_PYODIDE_IMAGE
|
||||
});
|
||||
statusBarsManager.changeTo('output');
|
||||
return statusBarsManager.get('images');
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-image.html').render());
|
||||
this.setContent($content);
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.hideCloseBtn();
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.getContent().empty();
|
||||
}
|
||||
|
||||
display(data) {
|
||||
const $content = this.getContent();
|
||||
const autoFit = function (node) {
|
||||
node.style.width = 'auto';
|
||||
node.style.height = 'auto';
|
||||
node.style.maxWidth = '100%';
|
||||
node.style.maxHeight = '100%';
|
||||
};
|
||||
this.clean();
|
||||
let root = data.content;
|
||||
let canvas = null;
|
||||
let iframe = null;
|
||||
switch (data.display_type) {
|
||||
case 'p5':
|
||||
root.style.width = '100%';
|
||||
root.style.height = '100%';
|
||||
root.style.display = 'flex';
|
||||
root.style.justifyContent = 'center';
|
||||
root.style.alignItems = 'center';
|
||||
|
||||
// some canvas nodes can be added later so we observe...
|
||||
new MutationObserver(function (mutationsList) {
|
||||
mutationsList.forEach((mutation) =>
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
const elem = node;
|
||||
if (
|
||||
elem.tagName != null &&
|
||||
['canvas', 'video'].includes(elem.tagName.toLowerCase())
|
||||
)
|
||||
autoFit(elem);
|
||||
})
|
||||
);
|
||||
}).observe(root, { childList: true });
|
||||
|
||||
root.querySelectorAll('canvas,video').forEach(autoFit);
|
||||
$content.append(root);
|
||||
break;
|
||||
case 'matplotlib':
|
||||
canvas = root.querySelector('canvas');
|
||||
if (canvas) root = canvas;
|
||||
root.style.width = '';
|
||||
root.style.height = '';
|
||||
root.style.maxWidth = '100%';
|
||||
root.style.maxHeight = '100%';
|
||||
$content.append(root);
|
||||
break;
|
||||
case 'ocaml-canvas':
|
||||
root.style.width = '';
|
||||
root.style.height = '';
|
||||
root.style.maxWidth = '100%';
|
||||
root.style.maxHeight = '100%';
|
||||
$content.append(root);
|
||||
break;
|
||||
case 'turtle':
|
||||
// Turtle result
|
||||
root.setAttribute('width', '100%');
|
||||
root.setAttribute('height', '100%');
|
||||
$content.append(root.outerHTML);
|
||||
break;
|
||||
case 'sympy':
|
||||
$content.append(data.content);
|
||||
if (typeof window.MathJax === 'undefined') {
|
||||
// dynamically loading MathJax
|
||||
console.log('Loading MathJax (Sympy expression needs it).');
|
||||
(function () {
|
||||
let script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src =
|
||||
'https://cdn.jsdelivr.net/npm/mathjax@3.0.5/es5/tex-mml-chtml.js';
|
||||
document.getElementsByTagName('head')[0].appendChild(script);
|
||||
})();
|
||||
} else {
|
||||
// otherwise, render it
|
||||
window.MathJax.typeset();
|
||||
}
|
||||
break;
|
||||
case 'multiple':
|
||||
/* typically dispached by display() */
|
||||
for (let mime of [
|
||||
'image/svg+xml',
|
||||
'image/png',
|
||||
'text/html',
|
||||
'text/plain',
|
||||
]) {
|
||||
if (mime in data.content) {
|
||||
let content = data.content[mime];
|
||||
if (mime === 'image/png') {
|
||||
content =
|
||||
'<img src="data:image/png;base64,' +
|
||||
content +
|
||||
'" style="max-width: 100%; max-height: 100%;">';
|
||||
}
|
||||
$content.append(content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'tutor':
|
||||
// hacky but iframe.document.body.style require to wait for
|
||||
// iframe loading
|
||||
$content.append($(data.content.replace('overflow-y%3A%20hidden%3B', '')));
|
||||
iframe = this.getContent()[0].getElementsByTagName('iframe')[0];
|
||||
if (iframe == null) return;
|
||||
// trick to avoid taking height update into account
|
||||
iframe.style.maxHeight = iframe.style.minHeight = '100%';
|
||||
|
||||
// force rendering when visible,
|
||||
// otherwise, strange things happends
|
||||
// since PythonTutor check for visibility at some point
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
const entry = entries[0];
|
||||
if (entry && !entry.isIntersecting) return;
|
||||
iframe.contentWindow?.postMessage({ type: 'redraw' }, '*');
|
||||
observer.disconnect();
|
||||
}).observe(iframe);
|
||||
|
||||
break;
|
||||
default:
|
||||
console.error(
|
||||
`Not supported node type '${data.display_type}' in eval.display result processing.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import $ from 'jquery';
|
||||
import { Msg } from 'blockly/core';
|
||||
import {
|
||||
PageBase,
|
||||
HTMLTemplate,
|
||||
StatusBarsManager,
|
||||
Workspace
|
||||
} from 'mixly';
|
||||
import '../language/loader';
|
||||
import STATUS_BAR_TOOL_TEMPLATE from '../templates/html/statusbar-tool.html';
|
||||
|
||||
|
||||
export default class StatusBarTool extends PageBase {
|
||||
static {
|
||||
HTMLTemplate.add(
|
||||
'html/statusbar/statusbar-tool.html',
|
||||
new HTMLTemplate(STATUS_BAR_TOOL_TEMPLATE)
|
||||
);
|
||||
|
||||
this.init = function () {
|
||||
StatusBarsManager.typesRegistry.register(['tool'], StatusBarTool);
|
||||
const mainWorkspace = Workspace.getMain();
|
||||
const statusBarsManager = mainWorkspace.getStatusBarsManager();
|
||||
statusBarsManager.add({
|
||||
type: 'tool',
|
||||
id: 'tool',
|
||||
name: Msg.PYTHON_PYODIDE_TOOL,
|
||||
title: Msg.PYTHON_PYODIDE_TOOL
|
||||
});
|
||||
statusBarsManager.changeTo('output');
|
||||
return statusBarsManager.get('tool');
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-tool.html').render());
|
||||
this.setContent($content);
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.hideCloseBtn();
|
||||
}
|
||||
|
||||
onMounted() { }
|
||||
|
||||
onUnmounted() { }
|
||||
|
||||
resize() { }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import { onMounted, provide, reactive, ref } from 'vue';
|
||||
import { ElConfigProvider } from 'element-plus';
|
||||
import teachableModel from './components/teachableModel.vue';
|
||||
|
||||
// import 'element-plus/theme-chalk/el-message.css';
|
||||
// import 'element-plus/theme-chalk/el-message-box.css';
|
||||
// import 'element-plus/theme-chalk/el-notification.css';
|
||||
// import './styles/index.scss';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider>
|
||||
<teachableModel />
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* #app {
|
||||
text-align: center;
|
||||
color: var(--ep-text-color-primary);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
height: calc(100vh - var(--ep-menu-item-height) - 4px);
|
||||
background-color: #feffff;
|
||||
} */
|
||||
</style>
|
||||
@@ -0,0 +1,213 @@
|
||||
<script setup>
|
||||
import { provide, ref } from 'vue';
|
||||
import { CirclePlusFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElSplitter, ElSplitterPanel, ElCard, ElButton, ElIcon } from 'element-plus';
|
||||
import ClassBox from './teachableModel/ClassBox.vue';
|
||||
import CameraBox from './teachableModel/CameraBox.vue';
|
||||
import ModelArea from './teachableModel/ModelArea.vue';
|
||||
import TrainArea from './teachableModel/TrainArea.vue';
|
||||
|
||||
// import 'element-plus/theme-chalk/src/message.scss';
|
||||
// import 'element-plus/theme-chalk/src/splitter.scss';
|
||||
// import 'element-plus/theme-chalk/src/splitter-panel.scss';
|
||||
// import 'element-plus/theme-chalk/src/card.scss';
|
||||
// import 'element-plus/theme-chalk/src/button.scss';
|
||||
// import 'element-plus/theme-chalk/src/icon.scss';
|
||||
|
||||
|
||||
// 存放所有拍摄的内容
|
||||
const shotList = ref([])
|
||||
provide('shotList', shotList)
|
||||
|
||||
// 存放类别和每个类别内含有的图片样本
|
||||
const picList = ref([
|
||||
{ title: '类别 1', list: [], disabled: false },
|
||||
{ title: '类别 2', list: [], disabled: false },
|
||||
])
|
||||
provide('picList', picList)
|
||||
|
||||
// 训练状态
|
||||
const states = ref({
|
||||
isTraining: 0, // 0为未开始,1为训练中,2为训练完成
|
||||
})
|
||||
provide('states', states)
|
||||
|
||||
// 向某个类别添加一个样本
|
||||
async function handleAddSample(idx, data) {
|
||||
// 类别已被禁用
|
||||
if (picList.value[idx].disabled) {
|
||||
ElMessage.error('该类别已被禁用')
|
||||
return
|
||||
}
|
||||
// 可能传入 base64 字符串上传图片
|
||||
if (data) {
|
||||
// data 是 base64,说明是上传图片
|
||||
picList.value[idx].list.push(data)
|
||||
return
|
||||
}
|
||||
// 否则是拍摄,调用摄像头拍摄
|
||||
await handleParentShot()
|
||||
// 检查是否成功拍摄到图片
|
||||
if (
|
||||
!shotList.value[shotList.value.length - 1]
|
||||
|| shotList.value[shotList.value.length - 1] === 'data:,'
|
||||
) {
|
||||
ElMessage.error('未获取到有效样本')
|
||||
return
|
||||
}
|
||||
// 拍摄到的图片添加到类别中
|
||||
picList.value[idx].list.push(shotList.value[shotList.value.length - 1])
|
||||
}
|
||||
|
||||
// 添加一个类别
|
||||
function handleAddClass() {
|
||||
picList.value.push({
|
||||
title: `类别 ${picList.value.length + 1}`,
|
||||
list: [],
|
||||
disabled: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 删除一个类别
|
||||
function handleDeleteClass(idx) {
|
||||
picList.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
// 禁用/启用一个类别
|
||||
function handleDisableClass(idx) {
|
||||
picList.value[idx].disabled = !picList.value[idx].disabled
|
||||
}
|
||||
|
||||
// 重命名一个类别
|
||||
function handleRenameClass(idx, newTitle) {
|
||||
picList.value[idx].title = newTitle
|
||||
}
|
||||
|
||||
// 清空一个类别的样本
|
||||
function handleClearSamples(idx) {
|
||||
picList.value[idx].list = []
|
||||
}
|
||||
|
||||
// 显示模型区域与否
|
||||
const showModelArea = ref(true)
|
||||
|
||||
// 令摄像头拍摄一张图片加入shotList
|
||||
const cameraBoxRef = ref(null)
|
||||
async function handleParentShot() {
|
||||
await cameraBoxRef.value?.captureShot()
|
||||
}
|
||||
|
||||
// 训练模型
|
||||
const modelAreaRef = ref(null)
|
||||
async function handleTrain() {
|
||||
await modelAreaRef.value.train()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-splitter">
|
||||
<ElSplitter>
|
||||
<ElSplitterPanel size="70%" :min="200">
|
||||
<div class="class-area">
|
||||
<ClassBox v-for="(item, idx) in picList" :key="idx" :title="item.title" :pic-list="item.list"
|
||||
:disabled="item.disabled" @add-sample="(data) => handleAddSample(idx, data)" @delete-sample="
|
||||
(url) => (picList[idx].list = picList[idx].list.filter((i) => i !== url))
|
||||
" @delete-class="handleDeleteClass(idx)" @disable-class="handleDisableClass(idx)"
|
||||
@rename-class="(newTitle) => handleRenameClass(idx, newTitle)"
|
||||
@clear-samples="handleClearSamples(idx)">
|
||||
<ElButton type="primary" size="mini" @click="handleAddSample(idx)">
|
||||
添加样本
|
||||
</ElButton>
|
||||
</ClassBox>
|
||||
<ElCard class="add-class-card" @click="handleAddClass">
|
||||
<div class="add-class-content">
|
||||
添加一个类别
|
||||
<ElIcon>
|
||||
<CirclePlusFilled />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ElSplitterPanel>
|
||||
<ElSplitterPanel :min="200">
|
||||
<CameraBox ref="cameraBoxRef" />
|
||||
<transition name="fade-scale">
|
||||
<ModelArea v-if="showModelArea" ref="modelAreaRef" @shot="handleParentShot" />
|
||||
</transition>
|
||||
<TrainArea @train="handleTrain" />
|
||||
</ElSplitterPanel>
|
||||
</ElSplitter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #011216;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
background: linear-gradient(to right, #d7dee1, transparent, transparent, transparent);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.teachable-model-container {
|
||||
width: 80%;
|
||||
height: 80vh;
|
||||
margin: 20px;
|
||||
padding: 5px;
|
||||
border: 2px solid #000;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 0px 10px #000;
|
||||
}
|
||||
|
||||
.main-splitter {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-shadow: var(--el-border-color-light) 0px 0px 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.add-class-card {
|
||||
margin: 10px auto;
|
||||
border: 1px dotted #000;
|
||||
max-width: 50vw;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.add-class-card:hover {
|
||||
cursor: pointer;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.fade-scale-enter-active {
|
||||
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
|
||||
.fade-scale-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
|
||||
.fade-scale-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.fade-scale-enter-to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.fade-scale-leave-from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.fade-scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script setup>
|
||||
import { defineExpose, inject, onMounted, ref } from 'vue';
|
||||
|
||||
const videoRef = ref(null)
|
||||
const shotList = inject('shotList')
|
||||
|
||||
// 初始化摄像头
|
||||
onMounted(() => {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: true })
|
||||
.then((stream) => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = stream
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('无法访问摄像头:', err)
|
||||
})
|
||||
})
|
||||
|
||||
// 拍摄照片,转为正方形,存入shotList
|
||||
function captureShot() {
|
||||
const video = videoRef.value
|
||||
if (!video)
|
||||
return
|
||||
const vw = video.videoWidth
|
||||
const vh = video.videoHeight
|
||||
let sx = 0
|
||||
let sy = 0
|
||||
let sw = vw
|
||||
let sh = vh
|
||||
if (vw > vh) {
|
||||
sx = (vw - vh) / 2
|
||||
sw = vh
|
||||
}
|
||||
else if (vh > vw) {
|
||||
sy = (vh - vw) / 2
|
||||
sh = vw
|
||||
}
|
||||
const size = Math.min(sw, sh)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size)
|
||||
shotList.value.push(canvas.toDataURL('image/png'))
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({ captureShot })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="camera-box">
|
||||
<video id="webcam" ref="videoRef" class="video-crop" autoplay playsinline muted height="100%" width="100%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.camera-box {
|
||||
margin: 10px auto;
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
width: 80%;
|
||||
max-width: 250px;
|
||||
aspect-ratio: 1/1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* background-color: black; */
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-crop {
|
||||
max-width: 250px;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.camera-box {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,357 @@
|
||||
<script setup>
|
||||
import { Camera, CirclePlus, Delete, EditPen, More } from '@element-plus/icons-vue';
|
||||
import { saveAs } from 'file-saver';
|
||||
import JSZip from 'jszip';
|
||||
import { nextTick, ref, watch, TransitionGroup } from 'vue';
|
||||
import { ElCard, ElDropdown, ElDropdownMenu, ElDropdownItem, ElButton, ElUpload, ElImage, ElInput, ElIcon } from 'element-plus';
|
||||
|
||||
// import 'element-plus/theme-chalk/el-card.css';
|
||||
// import 'element-plus/theme-chalk/el-dropdown.css';
|
||||
// import 'element-plus/theme-chalk/el-dropdown-menu.css';
|
||||
// import 'element-plus/theme-chalk/el-dropdown-item.css';
|
||||
// import 'element-plus/theme-chalk/el-button.css';
|
||||
// import 'element-plus/theme-chalk/el-upload.css';
|
||||
// import 'element-plus/theme-chalk/el-image.css';
|
||||
// import 'element-plus/theme-chalk/el-input.css';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '类别',
|
||||
},
|
||||
picList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'add-sample',
|
||||
'delete-sample',
|
||||
'rename-class',
|
||||
'delete-class',
|
||||
'disable-class',
|
||||
'clear-samples',
|
||||
])
|
||||
|
||||
const localTitle = ref(props.title)
|
||||
const editing = ref(false)
|
||||
const inputTitle = ref(localTitle.value)
|
||||
const inputRef = ref(null)
|
||||
const addSampleTimer = ref(null)
|
||||
|
||||
function editTitle() {
|
||||
if (!editing.value) {
|
||||
editing.value = true
|
||||
inputTitle.value = localTitle.value
|
||||
nextTick(() => {
|
||||
if (inputRef.value && inputRef.value.input) {
|
||||
inputRef.value.input.select()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function saveTitle() {
|
||||
if (inputTitle.value.trim()) {
|
||||
localTitle.value = inputTitle.value.trim()
|
||||
emit('rename-class', localTitle.value)
|
||||
}
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
function handleAddSample() {
|
||||
emit('add-sample')
|
||||
}
|
||||
|
||||
function handleDeleteSample(url) {
|
||||
emit('delete-sample', url)
|
||||
}
|
||||
|
||||
function startAddSample() {
|
||||
handleAddSample()
|
||||
addSampleTimer.value = setInterval(() => {
|
||||
handleAddSample()
|
||||
}, 200) // 200ms 可根据需要调整
|
||||
}
|
||||
|
||||
function stopAddSample() {
|
||||
if (addSampleTimer.value) {
|
||||
clearInterval(addSampleTimer.value)
|
||||
addSampleTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.title,
|
||||
(val) => {
|
||||
localTitle.value = val
|
||||
},
|
||||
)
|
||||
|
||||
function handleDeleteClass() {
|
||||
emit('delete-class')
|
||||
}
|
||||
function handleDisableClass() {
|
||||
emit('disable-class')
|
||||
}
|
||||
function handleClearSamples() {
|
||||
emit('clear-samples')
|
||||
}
|
||||
|
||||
function handleExportSamples() {
|
||||
// 导出为zip
|
||||
const zip = new JSZip()
|
||||
props.picList.forEach((url, idx) => {
|
||||
const base64 = url.split(',')[1]
|
||||
zip.file(`sample_${idx + 1}.png`, base64, { base64: true })
|
||||
})
|
||||
zip.generateAsync({ type: 'blob' }).then((content) => {
|
||||
saveAs(content, 'samples.zip')
|
||||
})
|
||||
}
|
||||
|
||||
// 新增:处理图片上传
|
||||
// 修改处理函数
|
||||
function handleUploadSample(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 新增ZIP文件处理
|
||||
if (file.type === 'application/zip' || file.name.endsWith('.zip')) {
|
||||
const zip = new JSZip()
|
||||
zip
|
||||
.loadAsync(file)
|
||||
.then((zipContents) => {
|
||||
const imageFiles = Object.values(zipContents.files).filter(
|
||||
file => !file.dir && file.name.match(/\.(png|jpg|jpeg|gif|bmp)$/i),
|
||||
)
|
||||
|
||||
Promise.all(
|
||||
imageFiles.map((zipFile) => {
|
||||
return zip
|
||||
.file(zipFile.name)
|
||||
.async('blob')
|
||||
.then((blob) => {
|
||||
const extractedFile = new File([blob], zipFile.name, {
|
||||
type: blob.type,
|
||||
})
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(extractedFile)
|
||||
return new Promise(r => (reader.onload = r))
|
||||
})
|
||||
}),
|
||||
).then((readers) => {
|
||||
readers.forEach(e => emit('add-sample', e.target.result))
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
.catch(reject)
|
||||
}
|
||||
else {
|
||||
// 原有图片处理逻辑
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
emit('add-sample', e.target.result)
|
||||
resolve(false)
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard class="class-box">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-header-title" style="cursor: pointer" @click="editTitle">
|
||||
<template v-if="!editing">
|
||||
{{ localTitle }}
|
||||
<ElIcon @click.stop="editTitle">
|
||||
<EditPen />
|
||||
</ElIcon>
|
||||
<span v-if="props.disabled" class="disabled-tag"
|
||||
style="color: #f56c6c; margin-left: 8px; font-size: 13px">
|
||||
禁用
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElInput ref="inputRef" v-model="inputTitle" style="width: 120px" @keyup.enter="saveTitle"
|
||||
@blur="saveTitle" />
|
||||
</template>
|
||||
</span>
|
||||
|
||||
<ElDropdown class="card-header-title">
|
||||
<span class="el-dropdown-link">
|
||||
<ElIcon>
|
||||
<More />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu class="class-box-dropdown">
|
||||
<ElDropdownItem class="class-box-dropdown-item" @click="handleDeleteClass">
|
||||
<span style="color: #f56c6c">删除类别</span>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem class="class-box-dropdown-item" @click="handleDisableClass">
|
||||
{{ props.disabled ? "启用" : "禁用" }}类别
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem class="class-box-dropdown-item" :disabled="props.picList.length === 0"
|
||||
@click="handleClearSamples">
|
||||
清空样本
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem class="class-box-dropdown-item" :disabled="props.picList.length === 0"
|
||||
@click="handleExportSamples">
|
||||
导出样本
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
<div class="card-content" style="flex-direction: column; display: flex">
|
||||
<div class="card-content-info">
|
||||
图像样本 {{ props.picList.length }}
|
||||
<div style="display: flex; gap: 4px">
|
||||
<ElButton type="primary" size="small" :disabled="props.disabled" @mousedown="startAddSample"
|
||||
@mouseup="stopAddSample" @mouseleave="stopAddSample">
|
||||
拍摄样本
|
||||
<ElIcon>
|
||||
<Camera />
|
||||
</ElIcon>
|
||||
</ElButton>
|
||||
<ElUpload :show-file-list="false" :before-upload="handleUploadSample" accept="image/*,.zip"
|
||||
:disabled="props.disabled">
|
||||
<ElButton plain type="primary" size="small" :disabled="props.disabled">
|
||||
上传样本
|
||||
<ElIcon>
|
||||
<CirclePlus />
|
||||
</ElIcon>
|
||||
</ElButton>
|
||||
</ElUpload>
|
||||
</div>
|
||||
</div>
|
||||
<TransitionGroup name="fade" tag="div" class="card-content-img">
|
||||
<div v-for="url in props.picList.slice().reverse()" :key="url" class="card-content-img-wrapper"
|
||||
style="position: relative; display: inline-block">
|
||||
<ElImage :src="url" class="card-content-img-item" :class="{ 'img-disabled': props.disabled }" />
|
||||
<ElIcon class="delete-icon" @click="handleDeleteSample(url)">
|
||||
<Delete />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.card-header-title:hover {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-content-img {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.card-content-img-item {
|
||||
margin: 2px;
|
||||
height: 60px;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.card-content-info {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.card-content-img-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 16px;
|
||||
z-index: 2;
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
transition: background 0.2s;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.delete-icon:hover {
|
||||
background: #f56c6c;
|
||||
}
|
||||
|
||||
.card-content-img-wrapper:hover .delete-icon {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.img-disabled {
|
||||
filter: grayscale(1) brightness(0.7);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.disabled-tag {
|
||||
color: #f56c6c;
|
||||
margin-left: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.class-box {
|
||||
margin: 10px auto;
|
||||
max-width: 50vw;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,517 @@
|
||||
<script setup>
|
||||
// import * as mobilenet from "@tensorflow-models/mobilenet";
|
||||
import * as tf from "@tensorflow/tfjs";
|
||||
import * as tfvis from "@tensorflow/tfjs-vis";
|
||||
import * as path from "path";
|
||||
import { inject, ref } from "vue";
|
||||
import {
|
||||
ElMessage,
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElRow,
|
||||
ElCol,
|
||||
ElInput,
|
||||
ElProgress,
|
||||
ElUpload,
|
||||
} from "element-plus";
|
||||
import { Env } from "mixly";
|
||||
|
||||
// import 'element-plus/theme-chalk/el-message.css';
|
||||
// import 'element-plus/theme-chalk/el-button.css';
|
||||
// import 'element-plus/theme-chalk/el-card.css';
|
||||
// import 'element-plus/theme-chalk/el-row.css';
|
||||
// import 'element-plus/theme-chalk/el-col.css';
|
||||
// import 'element-plus/theme-chalk/el-input.css';
|
||||
// import 'element-plus/theme-chalk/el-progress.css';
|
||||
// import 'element-plus/theme-chalk/el-upload.css';
|
||||
|
||||
const emit = defineEmits(["shot"]);
|
||||
// 类别及其样本的列表
|
||||
const picList = inject("picList");
|
||||
// 图片列表
|
||||
const shotList = inject("shotList");
|
||||
// 训练状态
|
||||
const states = inject("states");
|
||||
|
||||
// 用来显示进度条和名称
|
||||
const classList = ref(
|
||||
picList.value.map((item, idx) => ({
|
||||
name: item.title,
|
||||
progress: 0,
|
||||
}))
|
||||
);
|
||||
const progressColors = ["#FF6F61", "#42A5F5", "#66BB6A", "#FFA726", "#AB47BC"];
|
||||
// 用来存储模型
|
||||
let featureExtractor;
|
||||
// 用来存储损失值
|
||||
let lossValues = [];
|
||||
// 存储被训练的模型
|
||||
let model;
|
||||
|
||||
// 单击训练按钮后训练模型
|
||||
async function train() {
|
||||
// 可视化相关
|
||||
const visPanel = document.getElementById("vis-left-panel");
|
||||
if (visPanel) visPanel.innerHTML = "训练准备中……";
|
||||
showVisPanel.value = true;
|
||||
console.log("正在加载Mobilenet……");
|
||||
// net = await mobilenet.load();
|
||||
featureExtractor = await tf.loadGraphModel(
|
||||
path.join(Env.boardDirPath, "teachableModel/model.json")
|
||||
);
|
||||
|
||||
console.log("Mobilenet加载完成。");
|
||||
if (visPanel) visPanel.innerHTML = "";
|
||||
|
||||
// 准备数据
|
||||
const NUM_CLASSES = picList.value.length;
|
||||
let xs = null;
|
||||
let ys = null;
|
||||
// 准备模型
|
||||
model = tf.sequential();
|
||||
// 添加全连接层
|
||||
model.add(
|
||||
tf.layers.dense({
|
||||
inputShape: [1280],
|
||||
units: 128,
|
||||
activation: "relu",
|
||||
})
|
||||
);
|
||||
// 添加分类层
|
||||
model.add(
|
||||
tf.layers.dense({
|
||||
units: NUM_CLASSES,
|
||||
activation: "softmax",
|
||||
})
|
||||
);
|
||||
// 编译模型
|
||||
model.compile({
|
||||
optimizer: tf.train.adam(0.001),
|
||||
loss: "categoricalCrossentropy",
|
||||
metrics: ["accuracy"],
|
||||
});
|
||||
|
||||
for (let classId = 0; classId < picList.value.length; classId++) {
|
||||
if (picList.value[classId].disabled) continue;
|
||||
const images = picList.value[classId].list;
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
// 加载图片
|
||||
const imgElement = new Image();
|
||||
imgElement.src = images[i];
|
||||
await new Promise((resolve) => {
|
||||
imgElement.onload = resolve;
|
||||
});
|
||||
// 将图片转化为张量
|
||||
const imgTensor = tf.browser.fromPixels(imgElement);
|
||||
|
||||
// 使用神经网络模型进行推理,获取名为"conv_preds"的卷积层激活值
|
||||
// let activation = net.infer(imgTensor, "conv_preds");
|
||||
// 加载特征提取器和你的模型
|
||||
|
||||
// 预处理图像并提取特征
|
||||
const preprocessedImg = imgTensor
|
||||
.resizeBilinear([224, 224])
|
||||
.toFloat()
|
||||
.div(tf.scalar(127.5))
|
||||
.sub(tf.scalar(1))
|
||||
.expandDims(0);
|
||||
|
||||
const features = featureExtractor.predict(preprocessedImg);
|
||||
|
||||
// 将特征输入你的模型
|
||||
// const predictions = net.predict(features);
|
||||
let activation = features;
|
||||
// if (activation.shape.length === 3) {
|
||||
// activation = activation.reshape([1, 1024]);
|
||||
// }
|
||||
// let activation = net.predict(imgTensor);
|
||||
// 检查激活值张量的维度,如果是3维张量则进行形状重塑
|
||||
// 3维形状通常为 [height, width, channels],重塑为 [1, 1024] 的张量
|
||||
// 为了适配后续分类层或特征可视化的输入要求
|
||||
|
||||
// 转换为one-hot编码
|
||||
const y = tf.oneHot(tf.tensor1d([classId]).toInt(), NUM_CLASSES);
|
||||
// 初始化xs和ys
|
||||
if (xs == null) {
|
||||
xs = activation.clone();
|
||||
ys = y.clone();
|
||||
} else {
|
||||
const oldXs = xs;
|
||||
xs = oldXs.concat(activation, 0);
|
||||
oldXs.dispose();
|
||||
|
||||
const oldYs = ys;
|
||||
ys = oldYs.concat(y, 0);
|
||||
oldYs.dispose();
|
||||
}
|
||||
y.dispose();
|
||||
imgTensor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// 训练过程可视化相关
|
||||
lossValues = [];
|
||||
const metrics = ["loss", "acc", "val_loss", "val_acc", "accuracy", "val_accuracy"];
|
||||
const container = document.getElementById("vis-left-panel") || {
|
||||
name: "训练过程",
|
||||
tab: "训练",
|
||||
};
|
||||
// 训练模型
|
||||
await model.fit(xs, ys, {
|
||||
epochs: 20,
|
||||
batchSize: 16,
|
||||
shuffle: true,
|
||||
validationSplit: 0.2,
|
||||
callbacks: tfvis.show.fitCallbacks(container, metrics, {
|
||||
callbacks: ["onEpochEnd"],
|
||||
}),
|
||||
});
|
||||
console.log("训练完成");
|
||||
// 与显示进度条相关
|
||||
classList.value = picList.value
|
||||
.filter((item) => item.disabled !== true)
|
||||
.map((item, idx) => ({
|
||||
name: item.title,
|
||||
progress: 0,
|
||||
}));
|
||||
console.log(classList.value);
|
||||
states.value.isTraining = 2;
|
||||
}
|
||||
|
||||
setInterval(async () => {
|
||||
// 没训练完成跳过
|
||||
if (states.value.isTraining !== 2) return;
|
||||
// 输入是“上传图片”时跳过
|
||||
if (uploadedImg.value !== "") return;
|
||||
// 通知摄像头拍摄照片
|
||||
emit("shot");
|
||||
// 加载拍摄的照片
|
||||
const img = shotList.value[shotList.value.length - 1];
|
||||
if (!img || img === "data:,") {
|
||||
ElMessage.error("未获取到有效样本");
|
||||
return;
|
||||
}
|
||||
const imgElement = new Image();
|
||||
imgElement.src = img;
|
||||
await new Promise((resolve) => {
|
||||
imgElement.onload = resolve;
|
||||
});
|
||||
// 将图片转换为张量
|
||||
const imgTensor = tf.browser.fromPixels(imgElement);
|
||||
// let activation = net.infer(imgTensor, "conv_preds");
|
||||
let resized = tf.image.resizeBilinear(imgTensor, [224, 224]);
|
||||
let batched = resized.expandDims(0);
|
||||
let normalized = batched.div(255);
|
||||
// let activation = net.predict(imgTensor);
|
||||
let activation = featureExtractor.predict(normalized);
|
||||
const pred = model.predict(activation);
|
||||
const predArr = await pred.data();
|
||||
// console.log(predArr)
|
||||
classList.value = [
|
||||
...classList.value.map((item, idx) => ({
|
||||
...item,
|
||||
progress: Number((predArr[idx] * 100).toFixed(2)),
|
||||
})),
|
||||
];
|
||||
// console.log(classList.value)
|
||||
imgTensor.dispose();
|
||||
activation.dispose();
|
||||
pred.dispose();
|
||||
}, 200);
|
||||
|
||||
const showVisPanel = ref(false);
|
||||
const uploadedImg = ref("");
|
||||
const uploadedResult = ref("");
|
||||
const panelTop = ref("20%");
|
||||
const panelRight = ref("20%");
|
||||
|
||||
async function exportModel() {
|
||||
if (!model) {
|
||||
ElMessage.error("模型尚未训练完成");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 导出模型到本地文件系统
|
||||
await model.save(
|
||||
`downloads://${modelName.value == "" ? "my-model" : modelName.value}`
|
||||
);
|
||||
ElMessage.success("模型导出成功");
|
||||
} catch (error) {
|
||||
ElMessage.error(`模型导出失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpload(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
uploadedImg.value = e.target.result;
|
||||
if (!featureExtractor || !model) {
|
||||
ElMessage.error("请先完成模型训练");
|
||||
return false;
|
||||
}
|
||||
const imgElement = new window.Image();
|
||||
imgElement.src = uploadedImg.value;
|
||||
await new Promise((resolve) => (imgElement.onload = resolve));
|
||||
const imgTensor = tf.browser.fromPixels(imgElement);
|
||||
// let activation = net.infer(imgTensor, "conv_preds");
|
||||
let resized = tf.image.resizeBilinear(imgTensor, [224, 224]);
|
||||
let batched = resized.expandDims(0);
|
||||
let normalized = batched.div(255);
|
||||
// let activation = net.predict(imgTensor);
|
||||
let activation = featureExtractor.predict(normalized);
|
||||
const pred = model.predict(activation);
|
||||
const predArr = await pred.data();
|
||||
classList.value = [
|
||||
...classList.value.map((item, idx) => ({
|
||||
...item,
|
||||
progress: Number((predArr[idx] * 100).toFixed(2)),
|
||||
})),
|
||||
];
|
||||
const maxIdx = predArr.indexOf(Math.max(...predArr));
|
||||
uploadedResult.value = classList.value[maxIdx]?.name || "未知";
|
||||
imgTensor.dispose();
|
||||
activation.dispose();
|
||||
pred.dispose();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
return false;
|
||||
}
|
||||
|
||||
function switchToUpload() {
|
||||
uploadedImg.value = "";
|
||||
}
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let startTop = 0;
|
||||
let startRight = 0;
|
||||
|
||||
function onDragStart(e) {
|
||||
if (e.button !== 0) return;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
const topVal = document.getElementById("vis-left-panel-wrapper").style.top;
|
||||
const rightVal = document.getElementById("vis-left-panel-wrapper").style.right;
|
||||
startTop = topVal.endsWith("%")
|
||||
? (window.innerHeight * Number.parseFloat(topVal)) / 100
|
||||
: Number.parseFloat(topVal);
|
||||
startRight = rightVal.endsWith("%")
|
||||
? (window.innerWidth * Number.parseFloat(rightVal)) / 100
|
||||
: Number.parseFloat(rightVal);
|
||||
|
||||
document.addEventListener("mousemove", onDragging);
|
||||
document.addEventListener("mouseup", onDragEnd);
|
||||
}
|
||||
|
||||
function onDragging(e) {
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
let newTop = startTop + deltaY;
|
||||
let newRight = startRight - deltaX;
|
||||
newTop = Math.max(0, Math.min(window.innerHeight - 100, newTop));
|
||||
newRight = Math.max(0, Math.min(window.innerWidth - 200, newRight));
|
||||
panelTop.value = `${newTop}px`;
|
||||
panelRight.value = `${newRight}px`;
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
document.removeEventListener("mousemove", onDragging);
|
||||
document.removeEventListener("mouseup", onDragEnd);
|
||||
}
|
||||
|
||||
defineExpose({ train });
|
||||
|
||||
const modelName = ref("");
|
||||
async function saveModel() {
|
||||
if (!model) {
|
||||
ElMessage.error("模型尚未训练完成");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 导出模型到本地文件系统
|
||||
await model.save(
|
||||
`indexeddb://${modelName.value == "" ? "my-model" : modelName.value}`
|
||||
);
|
||||
ElMessage.success("模型保存成功");
|
||||
} catch (error) {
|
||||
ElMessage.error(`模型保存失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-show="showVisPanel"
|
||||
id="vis-left-panel-wrapper"
|
||||
class="vis-left-panel-wrapper"
|
||||
:style="`right: ${panelRight}; top: ${panelTop};`"
|
||||
>
|
||||
<div class="vis-left-panel-inner-wrapper">
|
||||
<div class="vis-left-panel-title" style="" @mousedown="onDragStart">
|
||||
<span> 训练过程可视化 </span>
|
||||
<ElButton size="small" plain @click="showVisPanel = false"> 隐藏 </ElButton>
|
||||
</div>
|
||||
<div id="vis-left-panel" class="vis-left-panel">训练准备中……</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElCard v-if="states.isTraining === 2" class="model-area">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
模型
|
||||
<ElButton
|
||||
v-if="!showVisPanel"
|
||||
size="small"
|
||||
style="margin-left: 10px"
|
||||
type="primary"
|
||||
plain
|
||||
@click="showVisPanel = true"
|
||||
>
|
||||
显示训练过程
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="states.isTraining === 2"
|
||||
size="small"
|
||||
style="margin-left: 10px"
|
||||
type="success"
|
||||
plain
|
||||
@click="exportModel"
|
||||
>
|
||||
模型导出至本地
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElRow class="model-item" style="flex-direction: column; align-items: flex-center">
|
||||
<ElRow>
|
||||
<ElCol :span="6" style="display: flex; align-items: center; text-align: right">
|
||||
名称:
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElInput v-model="modelName" placeholder="请输入模型名称" />
|
||||
</ElCol>
|
||||
<ElCol :span="4" style="display: flex; align-items: center">
|
||||
<ElButton
|
||||
style="margin: auto 5px"
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
@click="saveModel"
|
||||
>
|
||||
保存
|
||||
</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElRow>
|
||||
<ElRow class="model-item">
|
||||
<b>输入</b>
|
||||
</ElRow>
|
||||
<ElRow class="model-item">
|
||||
<div v-if="!uploadedImg" style="margin: auto">
|
||||
上方拍摄内容
|
||||
<ElUpload
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:before-upload="handleUpload"
|
||||
>
|
||||
<ElButton size="small" type="success"> 切换为上传图片 </ElButton>
|
||||
</ElUpload>
|
||||
</div>
|
||||
<div v-else style="margin: auto">
|
||||
下方上传图片 <br />
|
||||
<ElButton size="small" type="success" plain @click="switchToUpload">
|
||||
切换为上方拍摄内容
|
||||
</ElButton>
|
||||
<ElUpload
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:before-upload="handleUpload"
|
||||
>
|
||||
<ElButton size="small" type="success"> 重新上传一张 </ElButton>
|
||||
</ElUpload>
|
||||
<img
|
||||
:src="uploadedImg"
|
||||
alt="用户上传图片"
|
||||
style="max-width: 100%; max-height: 150px; border-radius: 10px"
|
||||
/>
|
||||
</div>
|
||||
</ElRow>
|
||||
<ElRow class="model-item">
|
||||
<b>输出</b>
|
||||
</ElRow>
|
||||
<ElRow
|
||||
v-for="(item, idx) in classList"
|
||||
:key="`${item.name}-${idx}`"
|
||||
class="model-item"
|
||||
>
|
||||
<ElCol :span="6">
|
||||
{{ item.name }}
|
||||
</ElCol>
|
||||
<ElCol :span="18">
|
||||
<ElProgress
|
||||
class="progress"
|
||||
:text-inside="true"
|
||||
:stroke-width="20"
|
||||
:percentage="item.progress"
|
||||
:color="progressColors[idx % progressColors.length]"
|
||||
striped
|
||||
:format="(p) => `${p}%`"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vis-left-panel-wrapper {
|
||||
transition: all 0.3s;
|
||||
position: fixed;
|
||||
width: 400px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 0 20px #0001;
|
||||
}
|
||||
|
||||
.vis-left-panel-inner-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px #0001;
|
||||
padding: 10px 10px 0 10px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.vis-left-panel-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.vis-left-panel {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.model-area {
|
||||
margin: 10px auto;
|
||||
width: 85%;
|
||||
max-width: 300px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.model-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup>
|
||||
import { inject, ref, watch } from 'vue';
|
||||
import { Warning } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElPopover, ElCard, ElText, ElIcon } from 'element-plus';
|
||||
|
||||
// import 'element-plus/theme-chalk/el-button.css';
|
||||
// import 'element-plus/theme-chalk/el-popover.css';
|
||||
// import 'element-plus/theme-chalk/el-card.css';
|
||||
// import 'element-plus/theme-chalk/el-text.css';
|
||||
// import 'element-plus/theme-chalk/el-icon.css';
|
||||
|
||||
|
||||
const emit = defineEmits(['train'])
|
||||
|
||||
// 获取类别及其样本
|
||||
const picList = inject('picList')
|
||||
// 同步训练状态
|
||||
const states = inject('states')
|
||||
|
||||
// 训练按钮是否可用
|
||||
const isTrainButtonEnabled = ref(false)
|
||||
const note_text = ref('待训练……')
|
||||
// 检查训练按钮是否可用
|
||||
function checkTrainButtonEnabled() {
|
||||
// 只考虑未被禁用的类别
|
||||
const enabledList = Array.isArray(picList.value)
|
||||
? picList.value.filter(item => item.disabled === false)
|
||||
: []
|
||||
return (
|
||||
states.value.isTraining !== 1
|
||||
&& enabledList.length > 1
|
||||
&& enabledList.every(item => Array.isArray(item.list) && item.list.length > 0)
|
||||
)
|
||||
}
|
||||
// 监听类别列表和训练状态变化,更新训练按钮的可用性
|
||||
watch(
|
||||
[picList.value, () => states.value.isTraining],
|
||||
() => {
|
||||
isTrainButtonEnabled.value = checkTrainButtonEnabled()
|
||||
if (states.value.isTraining === 0) {
|
||||
note_text.value = '待训练……'
|
||||
}
|
||||
else if (states.value.isTraining === 1) {
|
||||
note_text.value = '训练中……'
|
||||
}
|
||||
else if (states.value.isTraining === 2) {
|
||||
note_text.value = '训练完成'
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 训练模型
|
||||
function handleTrain() {
|
||||
if (isTrainButtonEnabled.value) {
|
||||
emit('train')
|
||||
states.value.isTraining = 1
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard class="train-area">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
训练
|
||||
</div>
|
||||
</template>
|
||||
<ElPopover class="box-item" placement="bottom" :disabled="isTrainButtonEnabled">
|
||||
<ElIcon>
|
||||
<Warning />
|
||||
</ElIcon>
|
||||
至少有两个类别,且每个类别都需有图像样本。
|
||||
<template #reference>
|
||||
<ElButton :disabled="!isTrainButtonEnabled" type="primary" style="max-width: 100%" @click="handleTrain">
|
||||
训练模型
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElPopover>
|
||||
<br>
|
||||
<ElText>{{ note_text }}</ElText>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.train-area {
|
||||
margin: 10px auto;
|
||||
width: 85%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
|
||||
export const isDark = useDark()
|
||||
export const toggleDark = useToggle(isDark)
|
||||
@@ -0,0 +1 @@
|
||||
export * from './dark'
|
||||
@@ -0,0 +1,8 @@
|
||||
// only scss variables
|
||||
|
||||
$--colors: (
|
||||
"primary": ("base": #589ef8,
|
||||
),
|
||||
);
|
||||
|
||||
@forward "element-plus/theme-chalk/src/dark/var.scss" with ($colors: $--colors);
|
||||
@@ -0,0 +1,33 @@
|
||||
$--colors: (
|
||||
'primary': ('base': green,
|
||||
),
|
||||
'success': ('base': #21ba45,
|
||||
),
|
||||
'warning': ('base': #f2711c,
|
||||
),
|
||||
'danger': ('base': #db2828,
|
||||
),
|
||||
'error': ('base': #db2828,
|
||||
),
|
||||
'info': ('base': #42b8dd,
|
||||
),
|
||||
);
|
||||
|
||||
// we can add this to custom namespace, default is 'el'
|
||||
@forward 'element-plus/theme-chalk/src/mixins/config.scss' with ($namespace: 'ep'
|
||||
);
|
||||
|
||||
// You should use them in scss, because we calculate it by sass.
|
||||
// comment next lines to use default color
|
||||
@forward 'element-plus/theme-chalk/src/common/var.scss' with ( // do not use same name, it will override.
|
||||
$colors: $--colors,
|
||||
$button-padding-horizontal: ('default': 50px));
|
||||
|
||||
// if you want to import all
|
||||
// @use "element-plus/theme-chalk/src/index.scss" as *;
|
||||
|
||||
// You can comment it to hide debug info.
|
||||
// @debug $--colors;
|
||||
|
||||
// custom dark variables
|
||||
@use './dark.scss';
|
||||
@@ -0,0 +1,28 @@
|
||||
// import dark theme
|
||||
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
|
||||
|
||||
// :root {
|
||||
// --ep-color-primary: red;
|
||||
// }
|
||||
|
||||
code {
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
background-color: var(--ep-color-primary-light-9);
|
||||
color: var(--ep-color-primary);
|
||||
}
|
||||
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: rgb(13, 148, 136);
|
||||
opacity: 0.75;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
58
mixly/boards/default_src/python_pyodide/others/tensorflow.js
Normal file
58
mixly/boards/default_src/python_pyodide/others/tensorflow.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Registry } from 'mixly';
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
|
||||
|
||||
const modelsValueRegistry = new Registry();
|
||||
const customFetch = function (path) {
|
||||
let result = {
|
||||
ok: false,
|
||||
buffer: null,
|
||||
json: function () {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const jsonText = decoder.decode(this.buffer);
|
||||
return JSON.parse(jsonText);
|
||||
},
|
||||
arrayBuffer: function () {
|
||||
return this.buffer;
|
||||
}
|
||||
}
|
||||
if (!modelsValueRegistry.hasKey(path)) {
|
||||
return result;
|
||||
}
|
||||
result.ok = true;
|
||||
result.buffer = modelsValueRegistry.getItem(path);
|
||||
return result;
|
||||
};
|
||||
|
||||
const tensorflow = {};
|
||||
tensorflow.modelsValue = {};
|
||||
|
||||
tensorflow.loadGraphModel = async function (path) {
|
||||
const model = await tf.loadGraphModel(path, {
|
||||
fromTFHub: false,
|
||||
fetchFunc: (...args) => {
|
||||
return customFetch(args[0]);
|
||||
}
|
||||
});
|
||||
return model;
|
||||
};
|
||||
|
||||
tensorflow.loadLayersModel = async function (path) {
|
||||
const model = await tf.loadLayersModel(path, {
|
||||
fromTFHub: false,
|
||||
fetchFunc: (...args) => {
|
||||
return customFetch(args[0]);
|
||||
}
|
||||
});
|
||||
return model;
|
||||
};
|
||||
|
||||
tensorflow.setModelsValue = function (path, value) {
|
||||
if (modelsValueRegistry.hasKey(path)) {
|
||||
modelsValueRegistry.unregister(path);
|
||||
}
|
||||
modelsValueRegistry.register(path, value);
|
||||
tensorflow.modelsValue[path] = value;
|
||||
};
|
||||
|
||||
window.tensorflow = tensorflow;
|
||||
Reference in New Issue
Block a user