Compare commits
8 Commits
laptop-bac
...
develop
Author | SHA1 | Date | |
---|---|---|---|
4d688ed625 | |||
295de3ee01 | |||
0212ab13a2 | |||
456267e200 | |||
ad9703eb8f | |||
2c2d2588fc | |||
d310cbb1a2 | |||
2f9d51b68c |
37
.github/workflows/build.yaml
vendored
Normal file
37
.github/workflows/build.yaml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: Build
|
||||
run-name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build NPM Project
|
||||
runs-on: ubuntu-latest
|
||||
container: node
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: ztimson/actions/clone@develop
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Build Project
|
||||
run: npm run build
|
||||
|
||||
tag:
|
||||
name: Tag Version
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
container: node
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: ztimson/actions/clone@develop
|
||||
|
||||
- name: Get Version Number
|
||||
run: echo "VERSION=$(cat package.json | grep version | grep -Eo ':.+' | grep -Eo '[[:alnum:]\.\/\-]+')" >> $GITHUB_ENV
|
||||
|
||||
- name: Tag Version
|
||||
uses: ztimson/actions/tag@develop
|
||||
with:
|
||||
tag: ${{env.VERSION}}
|
11
LICENSE
Normal file
11
LICENSE
Normal file
@ -0,0 +1,11 @@
|
||||
Copyright (c) 2023 Zakary Timson
|
||||
|
||||
All Rights Reserved.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
73
README.md
73
README.md
@ -1,5 +1,72 @@
|
||||
# TSElectronTemplate
|
||||
This is a minimalist setup to get TypeScript & Electron working. It includes nothing extra.
|
||||
<!-- Header -->
|
||||
<div id="top" align="center">
|
||||
<br />
|
||||
|
||||
<!-- Logo -->
|
||||
<img src="https://git.zakscode.com/repo-avatars/c3df2b0fe775af37194771569752d82e54c81ba8a5493540bf14ec6bf1eadd9d" alt="Logo" width="200" height="200">
|
||||
|
||||
<!-- Title -->
|
||||
### Desktop Daemon
|
||||
|
||||
<!-- Description -->
|
||||
Virtual Desktop Pets
|
||||
|
||||
<!-- Repo badges -->
|
||||
[![Version](https://img.shields.io/badge/dynamic/json.svg?label=Version&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/desktop-daemon/tags&query=$[0].name)](https://git.zakscode.com/ztimson/desktop-daemon/tags)
|
||||
[![Pull Requests](https://img.shields.io/badge/dynamic/json.svg?label=Pull%20Requests&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/desktop-daemon&query=open_pr_counter)](https://git.zakscode.com/ztimson/desktop-daemon/pulls)
|
||||
[![Issues](https://img.shields.io/badge/dynamic/json.svg?label=Issues&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/desktop-daemon&query=open_issues_count)](https://git.zakscode.com/ztimson/desktop-daemon/issues)
|
||||
|
||||
<!-- Links -->
|
||||
|
||||
---
|
||||
<div>
|
||||
<a href="https://git.zakscode.com/ztimson/desktop-daemon/releases" target="_blank">Release Notes</a>
|
||||
• <a href="https://git.zakscode.com/ztimson/desktop-daemon/issues/new?template=.github%2fissue_template%2fbug.md" target="_blank">Report a Bug</a>
|
||||
• <a href="https://git.zakscode.com/ztimson/desktop-daemon/issues/new?template=.github%2fissue_template%2fenhancement.md" target="_blank">Request a Feature</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
[[_TOC_]]
|
||||
- [Template](#top)
|
||||
- [About](#about)
|
||||
- [Built With](#built-with)
|
||||
- [Setup](#setup)
|
||||
- [Development](#development)
|
||||
- [License](#license)
|
||||
|
||||
## About
|
||||
|
||||
<img alt="Screenshot" src="./screenshot.gif" width="60%" height="auto">
|
||||
|
||||
Desktop Daemon is an experimental game I used to learn sprites. You can create customziable pets which live on your desktop. They can largly be ignored as they play along your taskbar but need attention from time to time.
|
||||
|
||||
DD was writen completely froms scratch using TypeScript and Electron.
|
||||
|
||||
### Built With
|
||||
[![Electron](https://img.shields.io/badge/Electron-47848F?style=for-the-badge&logo=electron&logoColor=white)](https://www.electronjs.org/)
|
||||
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
|
||||
|
||||
## Setup
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h3 id="development" style="display: inline">
|
||||
Development
|
||||
</h3>
|
||||
</summary>
|
||||
|
||||
#### Prerequisites
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
|
||||
#### Instructions
|
||||
1. Install the dependencies: `npm install`
|
||||
2. Start the Angular server: `npm run start`
|
||||
|
||||
</details>
|
||||
|
||||
## License
|
||||
Copyright © 2023 Zakary Timson | All Rights Reserved
|
||||
|
||||
See the [license](./LICENSE) for more information.
|
||||
|
BIN
screenshot.gif
Normal file
BIN
screenshot.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
@ -19,7 +19,7 @@ export class NPC {
|
||||
private sprite!: any;
|
||||
private text: string;
|
||||
|
||||
public pos = [0, 0];
|
||||
public pos = [1000, 0];
|
||||
public vel = [0, 0];
|
||||
|
||||
constructor(private readonly ctx: CanvasRenderingContext2D,
|
||||
|
12716
v2/package-lock.json
generated
12716
v2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,2 +0,0 @@
|
||||
export const FRAME_RATE = 10;
|
||||
export const GRAVITY = 9.8;
|
@ -1,23 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Desktop Daemon</title>
|
||||
<link rel="icon" type="img/png" src="assets/logo.png">
|
||||
<title>Hello World!</title>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<h1>💖 Hello World!</h1>
|
||||
<p>Welcome to your Electron application.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,45 +1,54 @@
|
||||
import {app, BrowserWindow, screen} from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
|
||||
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
|
||||
// whether you're running in development or production).
|
||||
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
|
||||
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
|
||||
|
||||
if(require('electron-squirrel-startup')) app.quit();
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (require('electron-squirrel-startup')) {
|
||||
// eslint-disable-line global-require
|
||||
app.quit();
|
||||
}
|
||||
|
||||
const createWindow = (): void => {
|
||||
const primaryDisplay = screen.getPrimaryDisplay();
|
||||
const height = 250;
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
x: 0,
|
||||
y: primaryDisplay.workArea.height - height,
|
||||
height: height,
|
||||
width: primaryDisplay.size.width,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
roundedCorners: false,
|
||||
resizable: false,
|
||||
fullscreen: false,
|
||||
alwaysOnTop: true,
|
||||
icon: path.join(__dirname, "../assets/logo.png"),
|
||||
title: 'Desktop Daemon',
|
||||
height: 600,
|
||||
width: 800,
|
||||
webPreferences: {
|
||||
// nodeIntegration: true,
|
||||
// contextIsolation: false,
|
||||
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
|
||||
},
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
|
||||
|
||||
// Open the DevTools.
|
||||
mainWindow.webContents.openDevTools();
|
||||
};
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow);
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and import them here.
|
||||
|
@ -1,84 +0,0 @@
|
||||
import {GRAVITY} from './constants';
|
||||
import {SpriteSheet} from './sprite-sheet';
|
||||
|
||||
export type NPCOptions = {
|
||||
bubbleOffset?: [number, number];
|
||||
noBounds?: boolean;
|
||||
noGravity?: boolean;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export class NPC {
|
||||
private readonly emojis = [
|
||||
0x1F600, 0x1F601, 0x1F603, 0x1F603, 0x1F604, 0x1F605, 0x1F606,
|
||||
0x1F607, 0x1F609, 0x1F60A, 0x1F642, 0x1F643, 0x1F355, 0x1F354,
|
||||
];
|
||||
private readonly texturePack: any;
|
||||
|
||||
private random: number = 0;
|
||||
private sprite!: any;
|
||||
private text: string;
|
||||
|
||||
public pos = [0, 0];
|
||||
public vel = [0, 0];
|
||||
|
||||
constructor(private readonly ctx: CanvasRenderingContext2D,
|
||||
public readonly spriteSheetPath: string,
|
||||
public readonly spriteDefPath: string,
|
||||
public readonly options: NPCOptions = {},
|
||||
) {
|
||||
this.texturePack = new SpriteSheet(this.ctx,
|
||||
'./assets/sprites/texture-pack/spritesheet.png',
|
||||
'../assets/sprites/texture-pack/spritesheet.json');
|
||||
|
||||
if(this.options.bubbleOffset == null) this.options.bubbleOffset = [0, 0];
|
||||
if(this.options.scale == null) this.options.scale = 1;
|
||||
this.sprite = new SpriteSheet(ctx, spriteSheetPath, spriteDefPath);
|
||||
|
||||
setInterval(() => {
|
||||
this.message(String.fromCodePoint(this.emojis[~~(Math.random() * this.emojis.length)]));
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
animate(name: string, reverse = false) {
|
||||
if(this.text) {
|
||||
const bubble = this.texturePack.definitions.sprites['bubble'];
|
||||
const x = this.pos[0] + this.options.bubbleOffset[0] * (reverse ? -1 : 1);
|
||||
const y = this.pos[1] + this.options.bubbleOffset[1];
|
||||
this.texturePack.drawSprite(x, y, 'bubble', reverse);
|
||||
this.ctx.save();
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.textBaseline = 'middle';
|
||||
this.ctx.font = '24px serif';
|
||||
this.ctx.fillText(this.text, x + (bubble.width / 2 * (reverse ? -1 : 1)), y + (bubble.height / 2));
|
||||
this.ctx.restore();
|
||||
}
|
||||
this.sprite.animate(this.pos[0], this.pos[1], name, reverse, this.options.scale);
|
||||
}
|
||||
|
||||
tick() {
|
||||
if(this.random <= 0) {
|
||||
const move = Math.random();
|
||||
if(move < 0.333) this.vel = [0, 0];
|
||||
else if(move < 0.666) this.vel = [-10, 0];
|
||||
else this.vel = [10, 0];
|
||||
this.random = Math.random() * 50;
|
||||
}
|
||||
this.random--;
|
||||
|
||||
this.pos = [this.pos[0] + this.vel[0], this.pos[1] + this.vel[1]];
|
||||
if(!this.options.noGravity) this.vel[1] -= GRAVITY;
|
||||
if(this.pos[1] < 0) this.pos[1] = 0;
|
||||
if(!this.options.noBounds) {
|
||||
if(this.pos[0] < 0) this.pos[0] = 0;
|
||||
if(this.pos[0] > this.ctx.canvas.width) this.pos[0] = this.ctx.canvas.width;
|
||||
}
|
||||
|
||||
this.animate(this.vel[0] == 0 ? 'idle' : 'walk', this.vel[0] < 0);
|
||||
}
|
||||
|
||||
message(text: string, ms = 5000) {
|
||||
setTimeout(() => this.text = null, ms);
|
||||
this.text = text;
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
// See the Electron documentation for details on how to use preload scripts:
|
||||
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
@ -1,28 +1,31 @@
|
||||
/**
|
||||
* This file will automatically be loaded by webpack and run in the "renderer" context.
|
||||
* To learn more about the differences between the "main" and the "renderer" context in
|
||||
* Electron, visit:
|
||||
*
|
||||
* https://electronjs.org/docs/latest/tutorial/process-model
|
||||
*
|
||||
* By default, Node.js integration in this file is disabled. When enabling Node.js integration
|
||||
* in a renderer process, please be aware of potential security implications. You can read
|
||||
* more about security risks here:
|
||||
*
|
||||
* https://electronjs.org/docs/tutorial/security
|
||||
*
|
||||
* To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration`
|
||||
* flag:
|
||||
*
|
||||
* ```
|
||||
* // Create the browser window.
|
||||
* mainWindow = new BrowserWindow({
|
||||
* width: 800,
|
||||
* height: 600,
|
||||
* webPreferences: {
|
||||
* nodeIntegration: true
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import './index.css';
|
||||
import {FRAME_RATE} from './constants';
|
||||
import {NPC} from './npc';
|
||||
|
||||
const canvas = <HTMLCanvasElement>document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const screenHeight = window.innerHeight;
|
||||
const screenWidth = window.innerWidth;
|
||||
|
||||
canvas.width = screenWidth;
|
||||
canvas.height = screenHeight;
|
||||
|
||||
function clearScreen() { ctx.clearRect(0, 0, screenWidth, screenHeight); }
|
||||
|
||||
const dog = new NPC(ctx, '/static/sprites/shadow-dog/spritesheet.png', '/static/sprites/shadow-dog/spritesheet.json', {
|
||||
bubbleOffset: [50, 75],
|
||||
scale: 0.25
|
||||
});
|
||||
|
||||
let frame = 0, once = true;
|
||||
setInterval(() => {
|
||||
requestAnimationFrame(() => {
|
||||
frame++;
|
||||
clearScreen();
|
||||
|
||||
dog.tick();
|
||||
})
|
||||
}, 1000 / FRAME_RATE);
|
||||
console.log('👋 This message is being logged by "renderer.js", included via webpack');
|
||||
|
@ -1,87 +0,0 @@
|
||||
export type SpriteDefinitions = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
animations?: {[key: string]: {y: number, frames: number}};
|
||||
sprites?: {[key: string]: {x: number, y: number, width?: number, height?: number}};
|
||||
}
|
||||
|
||||
export class SpriteSheet {
|
||||
private readonly definitions!: SpriteDefinitions;
|
||||
private readonly spriteSheet!: HTMLImageElement;
|
||||
|
||||
private animation = '';
|
||||
private frame = -1;
|
||||
|
||||
constructor(private readonly ctx: CanvasRenderingContext2D,
|
||||
public readonly spriteSheetPath: string,
|
||||
public readonly spriteDefPath: string,
|
||||
) {
|
||||
this.spriteSheet = new Image();
|
||||
this.spriteSheet.src = spriteSheetPath;
|
||||
this.definitions = require(spriteDefPath);
|
||||
|
||||
// Backfill coordinates if shorthand is used
|
||||
if(this.definitions.sprites?.length) {
|
||||
Object.entries(this.definitions.sprites).forEach(sprite => {
|
||||
const [name, def] = sprite;
|
||||
if(def.width == null) {
|
||||
if(this.definitions.width == null)
|
||||
throw new Error(`Sprite is missing it's width: ${name}`);
|
||||
this.definitions.sprites[name].x = this.definitions.width * def.x;
|
||||
this.definitions.sprites[name].width = this.definitions.width;
|
||||
}
|
||||
if(def.height == null) {
|
||||
if(this.definitions.height == null)
|
||||
throw new Error(`Sprite is missing it's height: ${name}`);
|
||||
this.definitions.sprites[name].y = this.definitions.height * def.y;
|
||||
this.definitions.sprites[name].height = this.definitions.height;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
animate(x: number, y: number, name: string = this.animation, flip = false, scale = 1) {
|
||||
const animation = this.definitions.animations[name];
|
||||
if(!animation) throw new Error(`Animation doesn't exist: ${name}`);
|
||||
if(name == this.animation) {
|
||||
this.frame++;
|
||||
if(this.frame >= animation.frames) this.frame = 0;
|
||||
} else {
|
||||
this.animation = name;
|
||||
this.frame = 0;
|
||||
}
|
||||
this.drawFrame(x, y, this.animation, this.frame, flip, scale);
|
||||
}
|
||||
|
||||
drawFrame(x: number, y: number, name: string, frame: number, flip = false, scale = 1, centerX = true, centerY = false) {
|
||||
const sprite = this.definitions.animations[name];
|
||||
if(!sprite) throw new Error(`Animation doesn't exist: ${name}`);
|
||||
this.ctx.save();
|
||||
this.ctx.translate(centerX ?
|
||||
this.definitions.width * scale / 2 * (flip ? 1 : -1) :
|
||||
flip ? this.definitions.width : 0,
|
||||
centerY ? this.definitions.height / -2 : 0);
|
||||
this.ctx.scale(flip ? -1 : 1, 1);
|
||||
this.ctx.drawImage(this.spriteSheet,
|
||||
frame * this.definitions.width, sprite.y * this.definitions.height, this.definitions.width, this.definitions.height,
|
||||
x * (flip ? -1 : 1), this.ctx.canvas.height - (this.definitions.height * scale + y),
|
||||
this.definitions.width * scale, this.definitions.height * scale);
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
drawSprite(x: number, y: number, name: string, flip = false, scale = 1, centerX = true, centerY = false) {
|
||||
const sprite = this.definitions.sprites[name];
|
||||
if(!sprite) throw new Error(`Sprite doesn't exist: ${name}`);
|
||||
this.ctx.save();
|
||||
this.ctx.translate(centerX ?
|
||||
this.definitions.width * scale / 2 * (flip ? 1 : -1) :
|
||||
flip ? this.definitions.width : 0,
|
||||
centerY ? this.definitions.height / -2 : 0);
|
||||
this.ctx.scale(flip ? -1 : 1, 1);
|
||||
this.ctx.drawImage(this.spriteSheet,
|
||||
sprite.x, sprite.y, sprite.width, sprite.height,
|
||||
x * (flip ? -1 : 1), this.ctx.canvas.height - (sprite.height * scale + y),
|
||||
sprite.width * scale, sprite.height * scale);
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export function sleep(ms: number) {
|
||||
return new Promise(res => setTimeout(res, ms));
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 1018 B |
@ -1,14 +0,0 @@
|
||||
{
|
||||
"width": 575,
|
||||
"height": 523,
|
||||
"animations": {
|
||||
"idle": {
|
||||
"y": 0,
|
||||
"frames": 6
|
||||
},
|
||||
"walk": {
|
||||
"y": 3,
|
||||
"frames": 8
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 1.8 MiB |
@ -1,11 +0,0 @@
|
||||
{
|
||||
"width": 50,
|
||||
"height": 50,
|
||||
"animations": [
|
||||
{
|
||||
"name": "idle",
|
||||
"y": 0,
|
||||
"frames": 8
|
||||
}
|
||||
]
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 228 B |
@ -1,58 +0,0 @@
|
||||
{
|
||||
"sprites": {
|
||||
"generic": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"text": {
|
||||
"x": 100,
|
||||
"y": 0,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"code": {
|
||||
"x": 200,
|
||||
"y": 0,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"zip": {
|
||||
"x": 300,
|
||||
"y": 0,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"image": {
|
||||
"x": 400,
|
||||
"y": 0,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"audio": {
|
||||
"x": 500,
|
||||
"y": 0,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"video": {
|
||||
"x": 600,
|
||||
"y": 0,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"pdf": {
|
||||
"x": 700,
|
||||
"y": 0,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"bubble": {
|
||||
"x": 0,
|
||||
"y": 100,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 28 KiB |
Loading…
Reference in New Issue
Block a user