diff --git a/Pixel-Canvas-API/.dclignore b/Pixel-Canvas-API/.dclignore new file mode 100644 index 000000000..7499ff37a --- /dev/null +++ b/Pixel-Canvas-API/.dclignore @@ -0,0 +1,20 @@ +.* +bin/*.map +package-lock.json +yarn-lock.json +build.json +export +tsconfig.json +tslint.json +node_modules +*.ts +*.tsx +.vscode +Dockerfile +dist +README.md +*.blend +*.fbx +*.zip +*.rar +src diff --git a/Pixel-Canvas-API/.github/workflows/ci.yml b/Pixel-Canvas-API/.github/workflows/ci.yml new file mode 100644 index 000000000..f3ccb9e3f --- /dev/null +++ b/Pixel-Canvas-API/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: test-build + +on: + push: + pull_request: + +jobs: + lint-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 18.x + uses: actions/setup-node@v1 + with: + node-version: 18.x + - name: install dependencies + run: npm install + - name: npm run build + run: npm run build diff --git a/Pixel-Canvas-API/.gitignore b/Pixel-Canvas-API/.gitignore new file mode 100644 index 000000000..24fe526b3 --- /dev/null +++ b/Pixel-Canvas-API/.gitignore @@ -0,0 +1,9 @@ +package-lock.json +*.js +node_modules +bin/ +.DS_Store +**/.DS_Store +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/Pixel-Canvas-API/.vscode/extensions.json b/Pixel-Canvas-API/.vscode/extensions.json new file mode 100644 index 000000000..38b0d6772 --- /dev/null +++ b/Pixel-Canvas-API/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["decentralandfoundation.decentraland-sdk7"] +} diff --git a/Pixel-Canvas-API/.vscode/launch.json b/Pixel-Canvas-API/.vscode/launch.json new file mode 100644 index 000000000..a3b7699af --- /dev/null +++ b/Pixel-Canvas-API/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use the Decentraland Editor extension of VSCode to debug the scene + // in chrome from VSCode + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Debug Decentraland in Chrome", + "url": "${command:decentraland-sdk7.commands.getDebugURL}", + "webRoot": "${workspaceFolder}/bin", + "sourceMapPathOverrides": { + "dcl:///*": "${workspaceFolder}/*" + } + } + ] +} diff --git a/Pixel-Canvas-API/README.md b/Pixel-Canvas-API/README.md new file mode 100644 index 000000000..75b0c106b --- /dev/null +++ b/Pixel-Canvas-API/README.md @@ -0,0 +1,72 @@ +## Pixel Canvas API + +A scene that fetches pixel color data from an API and displays it on a giant canvas. Set your own pixel on the canvas and sync it with an online database via API. + + + +![](images/screenshot.jpg) + + +This scene shows you: + +- How to call a REST API and parse a JSON response +- How to store data in an online databse +- How to detect if player points on an entity +- How to update scene via messageBus +- How to use components to manage entities + + + +## Try it out + +**Install the CLI** + +Download and install the Decentraland CLI by running the following command: + +```bash +npm i -g decentraland +``` + +**Previewing the scene** + + 1. Download this full repository from [sdk7-goerli-plaza](https://github.com/decentraland/sdk7-goerli-plaza/tree/main), including this and several other example scenes on SDK7. + 2. Install the [Decentraland Editor](https://docs.decentraland.org/creator/development-guide/sdk7/editor/) + 3. Open a Visual Studio Code window on this scene's root folder. Not on the root folder of the whole repo, but instead on this sub-folder that belongs to the scene. + 4. Open the Decentraland Editor tab, and press **Run Scene** + +Alternatively, you can use the command line. Inside this scene root directory run: + +```bash + +npm run start + +``` + +## Scene Setup + +1. Create an account on [restdb.io](https://restdb.io/) + +2. Set up a table in the database with the required fields. Restdb.io will autogenerate a REST API for your table. + +The table for this scene should have the following format: + +```javascript +posX: number +posY: number +color: string +``` + +***CORS Configuration*** +To allow access from a Decentraland scene: + + +1. Navigate to the API tab under settings on restdb.io. +2. Create a new "Web page API key (CORS)". +3. Replace the `apiKey` value in `api.ts` with your generated API key. + + + + +## Copyright info + +This scene is protected with a standard Apache 2 licence. See the terms and conditions in the [LICENSE](/LICENSE) file. diff --git a/Pixel-Canvas-API/assets/scene/main.composite b/Pixel-Canvas-API/assets/scene/main.composite new file mode 100644 index 000000000..1c0170970 --- /dev/null +++ b/Pixel-Canvas-API/assets/scene/main.composite @@ -0,0 +1,262 @@ +{ + "version": 1, + "components": [ + { + "name": "core::Transform", + "jsonSchema": { + "type": "object", + "properties": { + "position": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "scale": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "rotation": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + }, + "w": { + "type": "number" + } + } + }, + "parent": { + "type": "integer" + } + }, + "serializationType": "transform" + }, + "data": { + "512": { + "json": { + "position": { + "x": 8, + "y": 1, + "z": 8 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "parent": 0 + } + } + } + }, + { + "name": "core::MeshRenderer", + "jsonSchema": { + "type": "object", + "properties": {}, + "serializationType": "protocol-buffer", + "protocolBuffer": "PBMeshRenderer" + }, + "data": { + "512": { + "json": { + "mesh": { + "$case": "box", + "box": { + "uvs": [] + } + } + } + } + } + }, + { + "name": "core-schema::Name", + "jsonSchema": { + "type": "object", + "properties": { + "value": { + "type": "string", + "serializationType": "utf8-string" + } + }, + "serializationType": "map" + }, + "data": { + "512": { + "json": { + "value": "Magic Cube" + } + } + } + }, + { + "name": "inspector::Scene", + "jsonSchema": { + "type": "object", + "properties": { + "layout": { + "type": "object", + "properties": { + "base": { + "type": "object", + "properties": { + "x": { + "type": "integer", + "serializationType": "int32" + }, + "y": { + "type": "integer", + "serializationType": "int32" + } + }, + "serializationType": "map" + }, + "parcels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "x": { + "type": "integer", + "serializationType": "int32" + }, + "y": { + "type": "integer", + "serializationType": "int32" + } + }, + "serializationType": "map" + }, + "serializationType": "array" + } + }, + "serializationType": "map" + } + }, + "serializationType": "map" + }, + "data": { + "0": { + "json": { + "layout": { + "base": { + "x": 0, + "y": 0 + }, + "parcels": [ + { + "x": 0, + "y": 0 + } + ] + } + } + } + } + }, + { + "name": "inspector::Nodes", + "jsonSchema": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "object", + "properties": { + "entity": { + "type": "integer", + "serializationType": "entity" + }, + "open": { + "type": "boolean", + "serializationType": "optional", + "optionalJsonSchema": { + "type": "boolean", + "serializationType": "boolean" + } + }, + "children": { + "type": "array", + "items": { + "type": "integer", + "serializationType": "entity" + }, + "serializationType": "array" + } + }, + "serializationType": "map" + }, + "serializationType": "array" + } + }, + "serializationType": "map" + }, + "data": { + "0": { + "json": { + "value": [ + { + "entity": 0, + "children": [ + 512 + ], + "open": true + }, + { + "entity": 512, + "children": [] + } + ] + } + } + } + }, + { + "name": "cube-id", + "jsonSchema": { + "type": "object", + "properties": {}, + "serializationType": "map" + }, + "data": { + "512": { + "json": {} + } + } + } + ] +} \ No newline at end of file diff --git a/Pixel-Canvas-API/images/scene-thumbnail.png b/Pixel-Canvas-API/images/scene-thumbnail.png new file mode 100644 index 000000000..b3dc58066 Binary files /dev/null and b/Pixel-Canvas-API/images/scene-thumbnail.png differ diff --git a/Pixel-Canvas-API/images/screenshot.jpg b/Pixel-Canvas-API/images/screenshot.jpg new file mode 100644 index 000000000..9d41a21d5 Binary files /dev/null and b/Pixel-Canvas-API/images/screenshot.jpg differ diff --git a/Pixel-Canvas-API/package.json b/Pixel-Canvas-API/package.json new file mode 100644 index 000000000..5d2598d7e --- /dev/null +++ b/Pixel-Canvas-API/package.json @@ -0,0 +1,26 @@ +{ + "name": "pixel-canvas-api", + "description": "Pixel art with friends", + "version": "1.0.0", + "devDependencies": { + "@dcl/js-runtime": "next", + "@dcl/sdk": "next" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=6.0.0" + }, + "prettier": { + "semi": false, + "singleQuote": true, + "printWidth": 120, + "trailingComma": "none" + }, + "scripts": { + "build": "sdk-commands build", + "deploy": "sdk-commands deploy", + "start": "sdk-commands start", + "upgrade-sdk": "npm install --save-dev @dcl/sdk@latest", + "upgrade-sdk:next": "npm install --save-dev @dcl/sdk@next" + } +} diff --git a/Pixel-Canvas-API/scene.json b/Pixel-Canvas-API/scene.json new file mode 100644 index 000000000..39f27c64b --- /dev/null +++ b/Pixel-Canvas-API/scene.json @@ -0,0 +1,54 @@ +{ + "ecs7": true, + "runtimeVersion": "7", + "display": { + "title": "Pixel-Canvas-API", + "description": "Pixel art with friends", + "navmapThumbnail": "images/scene-thumbnail.png", + "favicon": "favicon_asset" + }, + "owner": "", + "contact": { + "name": "SDK", + "email": "" + }, + "main": "bin/index.js", + "tags": [], + "scene": { + "parcels": [ + "76,9" + ], + "base": "76,9" + }, + "spawnPoints": [ + { + "name": "spawn1", + "default": true, + "position": { + "x": [ + 0, + 3 + ], + "y": [ + 0, + 0 + ], + "z": [ + 0, + 3 + ] + }, + "cameraTarget": { + "x": 8, + "y": 1, + "z": 8 + } + } + ], + "requiredPermissions": [ + "ALLOW_TO_TRIGGER_AVATAR_EMOTE", + "ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE" + ], + "featureToggles": {}, + "name": "Pixel-Canvas-API" +} \ No newline at end of file diff --git a/Pixel-Canvas-API/sounds/click_1.mp3 b/Pixel-Canvas-API/sounds/click_1.mp3 new file mode 100644 index 000000000..d62062047 Binary files /dev/null and b/Pixel-Canvas-API/sounds/click_1.mp3 differ diff --git a/Pixel-Canvas-API/sounds/click_2.mp3 b/Pixel-Canvas-API/sounds/click_2.mp3 new file mode 100644 index 000000000..a3c4a0c2d Binary files /dev/null and b/Pixel-Canvas-API/sounds/click_2.mp3 differ diff --git a/Pixel-Canvas-API/sounds/click_bright_001.mp3 b/Pixel-Canvas-API/sounds/click_bright_001.mp3 new file mode 100644 index 000000000..bf38562c1 Binary files /dev/null and b/Pixel-Canvas-API/sounds/click_bright_001.mp3 differ diff --git a/Pixel-Canvas-API/sounds/click_bright_002.mp3 b/Pixel-Canvas-API/sounds/click_bright_002.mp3 new file mode 100644 index 000000000..dd16bbcc1 Binary files /dev/null and b/Pixel-Canvas-API/sounds/click_bright_002.mp3 differ diff --git a/Pixel-Canvas-API/sounds/pop_2.mp3 b/Pixel-Canvas-API/sounds/pop_2.mp3 new file mode 100644 index 000000000..66cac4ab9 Binary files /dev/null and b/Pixel-Canvas-API/sounds/pop_2.mp3 differ diff --git a/Pixel-Canvas-API/src/api.ts b/Pixel-Canvas-API/src/api.ts new file mode 100644 index 000000000..a031961b8 --- /dev/null +++ b/Pixel-Canvas-API/src/api.ts @@ -0,0 +1,95 @@ +// API key here +const apiKey = '6518855968885408010c01fc' +const urlRestAPI = 'https://goerliplasapixelcanv-9b7d.restdb.io/rest/pixel' + +// Gets all pixel from database via GET API call +export function getDatabase(): Promise { + const url = urlRestAPI + + return fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'x-apikey': apiKey, + 'cache-control': 'no-cache' + } + }) + .then((response) => response.json()) + .then((entries) => { + // These are all entries in the database + // console.log(entries); + return entries + }) + .catch((error) => { + console.error('Error:', error) + return + }) +} + +// Creates new empty pixel data in the database +export function initDatabase(canvasWidth: number, canvasHeight: number, color: string): Promise { + const url = urlRestAPI + const pixels = [] + + // Create array of one colored pixels + for (let x = 0; x < canvasWidth; x++) { + for (let y = 0; y < canvasHeight; y++) { + const pixelData = { + posX: x, + posY: y, + color: color + } + pixels.push(pixelData) + } + } + + // Post whole array into Database at one + return fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-apikey': apiKey, + 'cache-control': 'no-cache' + }, + body: JSON.stringify(pixels) + }) + .then((response) => response.json()) + .then((data) => { + // Data is all the created entries in the database + return data + }) + .catch((error) => { + console.error('Error:', error) + throw error + }) +} + +// Update pixel database with new hex color code +export function updateDatabase(id: string, posX: number, posY: number, color: string) { + const url = urlRestAPI + const data = { + posX: posX, + posY: posY, + color: color + } + + // PUT overrides the pixel in database selected by id + return fetch(url + '/' + id, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'x-apikey': apiKey, + 'cache-control': 'no-cache' + }, + body: JSON.stringify(data) + }) + .then((response) => response.json()) + .then((data) => { + // Returns the newly created pixel data + return data + }) + .catch((error) => { + console.error('Error:', error) + return + }) +} diff --git a/Pixel-Canvas-API/src/components.ts b/Pixel-Canvas-API/src/components.ts new file mode 100644 index 000000000..c376cea03 --- /dev/null +++ b/Pixel-Canvas-API/src/components.ts @@ -0,0 +1,20 @@ +import { Schemas, engine } from '@dcl/sdk/ecs' + +// Flag for pixel entity +// Holds the id this pixel in the database +export const Pixel = engine.defineComponent('pixel', { + id: Schemas.String +}) + +// Flag for color picker entity +export const ColorPicker = engine.defineComponent('colorPicker', {}) + +// The current color of an entity +export const HexColor = engine.defineComponent('hexColor', { + hexColor: Schemas.String +}) + +// Flag for loading indicator, holds status: 'loading' or 'finished' +export const LoadingIndicator = engine.defineComponent('loadingIndicator', { + status: Schemas.String +}) diff --git a/Pixel-Canvas-API/src/factory.ts b/Pixel-Canvas-API/src/factory.ts new file mode 100644 index 000000000..a20c1a031 --- /dev/null +++ b/Pixel-Canvas-API/src/factory.ts @@ -0,0 +1,231 @@ +import { + AudioSource, + Entity, + InputAction, + Material, + MeshCollider, + MeshRenderer, + PointerEventType, + PointerEvents, + Transform, + TransformType, + engine, + executeTask, + pointerEventsSystem +} from '@dcl/sdk/ecs' +import { Color3, Color4 } from '@dcl/sdk/math' +import { canvas, playersColor, updatePlayersColor } from '.' +import { updateDatabase } from './api' +import { ColorPicker, HexColor, LoadingIndicator, Pixel } from './components' +import { sceneMessageBus } from './messageBus' + +// Create Canvas, this is a parent object holding pixels +export function createCanvas(transform: Partial): Entity { + const canvas = engine.addEntity() + Transform.create(canvas, transform) + return canvas +} + +// Create Pixel child of Canvas +export function createPixel(canvas: Entity, posX: number, posY: number, hexColor: string, id: string) { + const pixel = engine.addEntity() + Transform.create(pixel, { + parent: canvas, + position: { x: posX, y: posY, z: 0 } + }) + MeshRenderer.setBox(pixel) + MeshCollider.setBox(pixel) + Material.setPbrMaterial(pixel, { + albedoColor: Color4.fromHexString(hexColor), + roughness: 0.5, + emissiveColor: Color3.fromHexString(hexColor), + emissiveIntensity: 0.5 + }) + + // Add custom components to pixel + Pixel.create(pixel, { id: id }) + HexColor.create(pixel, { hexColor: hexColor }) + + // This component tracks if players pointer hovers over entity + PointerEvents.create(pixel, { + pointerEvents: [ + { + eventType: PointerEventType.PET_HOVER_ENTER, + eventInfo: { + button: InputAction.IA_POINTER, + showFeedback: false + } + }, + { + eventType: PointerEventType.PET_HOVER_LEAVE, + eventInfo: { + button: InputAction.IA_POINTER, + showFeedback: false + } + } + ] + }) + + // On Click + pointerEventsSystem.onPointerDown( + { + entity: pixel, + opts: { + button: InputAction.IA_POINTER, + hoverText: 'paint pixel' + } + }, + function () { + const id = Pixel.get(pixel).id + const pos = Transform.get(pixel).position + + // Play sound + AudioSource.createOrReplace(pixel, { + audioClipUrl: 'sounds/click_bright_002.mp3', + playing: true + }) + + createLoadingIndicator(pos.x, pos.y, playersColor, 'loading') + + // Write pixel color into database via API + executeTask(async () => { + const updatedPixel = await updateDatabase(id, pos.x, pos.y, playersColor) + console.log(updatedPixel) + if (!updatedPixel) return + + // Send new pixel data to all players in scene + sceneMessageBus.emit('updatePixelColor', { + posX: posX, + posY: posY, + hexColor: playersColor + }) + }) + } + ) +} + +// This parent holds all the color entities +export function createColorPicker(transform: Partial) { + const colorPicker = engine.addEntity() + Transform.create(colorPicker, transform) + return colorPicker +} + +// Create cubes to pick color from +export function createColor(parent: Entity, x: number, y: number, hexColor: string) { + const colorEntity = engine.addEntity() + MeshRenderer.setBox(colorEntity) + MeshCollider.setBox(colorEntity) + Transform.create(colorEntity, { + parent: parent, + position: { x: x, y: y, z: 0 } + }) + Material.setPbrMaterial(colorEntity, { + albedoColor: Color4.fromHexString(hexColor), + roughness: 0.5, + emissiveColor: Color3.fromHexString(hexColor), + emissiveIntensity: 0.5 + }) + HexColor.create(colorEntity, { hexColor: hexColor }) + ColorPicker.create(colorEntity) + + // This component tracks if players pointer hovers over entity + PointerEvents.create(colorEntity, { + pointerEvents: [ + { + eventType: PointerEventType.PET_HOVER_ENTER, + eventInfo: { + button: InputAction.IA_POINTER, + showFeedback: false + } + }, + { + eventType: PointerEventType.PET_HOVER_LEAVE, + eventInfo: { + button: InputAction.IA_POINTER, + showFeedback: false + } + } + ] + }) + + // On Click + pointerEventsSystem.onPointerDown( + { + entity: colorEntity, + opts: { + button: InputAction.IA_POINTER, + hoverText: 'pick color' + } + }, + function () { + const newColor: string = HexColor.get(colorEntity).hexColor + console.log('Players new color:', newColor) + updatePlayersColor(newColor) + + // Indicates click by moving cube away + Transform.getMutable(colorEntity).position.z = 0.5 + + // Play sound + AudioSource.createOrReplace(colorEntity, { + audioClipUrl: 'sounds/click_bright_001.mp3', + playing: true + }) + } + ) +} + +/** + * + * @param status 'loading' or 'finished' + */ +export function createLoadingIndicator(x: number, y: number, hexColor: string, status: string) { + // Remove indicator if it already exists + const indicators = engine.getEntitiesWith(LoadingIndicator) + for (const [entity] of indicators) { + const indicator = LoadingIndicator.getMutable(entity) + const position = Transform.get(entity).position + const thisHexColor = HexColor.get(entity).hexColor + if (position.x == x && position.y == y && thisHexColor == hexColor) { + engine.removeEntity(entity) + } + } + + // Set transparancy + let color = Color4.fromHexString(hexColor) + color.a = 0.5 + + // Create front indicator + const indicatorFront = engine.addEntity() + MeshRenderer.setSphere(indicatorFront) + Transform.create(indicatorFront, { + parent: canvas, + position: { x: x, y: y, z: 0.5 }, + scale: { x: 0.1, y: 0.1, z: 0.1 } + }) + Material.setPbrMaterial(indicatorFront, { + albedoColor: color, + roughness: 0.5, + emissiveColor: Color3.fromHexString(hexColor), + emissiveIntensity: 0.5 + }) + LoadingIndicator.create(indicatorFront, { status: status }) + HexColor.create(indicatorFront, { hexColor: hexColor }) + + // Create back indicator + const indicatorBack = engine.addEntity() + MeshRenderer.setSphere(indicatorBack) + Transform.create(indicatorBack, { + parent: canvas, + position: { x: x, y: y, z: -0.5 }, + scale: { x: 0.1, y: 0.1, z: 0.1 } + }) + Material.setPbrMaterial(indicatorBack, { + albedoColor: color, + roughness: 0.5, + emissiveColor: Color3.fromHexString(hexColor), + emissiveIntensity: 0.5 + }) + LoadingIndicator.create(indicatorBack, { status: status }) + HexColor.create(indicatorBack, { hexColor: hexColor }) +} diff --git a/Pixel-Canvas-API/src/index.ts b/Pixel-Canvas-API/src/index.ts new file mode 100644 index 000000000..0b143a015 --- /dev/null +++ b/Pixel-Canvas-API/src/index.ts @@ -0,0 +1,83 @@ +// color pallette +// Screenshot! + +import { Entity, engine } from '@dcl/sdk/ecs' +import { getDatabase, initDatabase } from './api' +import { Quaternion } from '@dcl/sdk/math' +import { createCanvas, createColor, createColorPicker, createPixel } from './factory' +import { colorPickerHoverSystem, loadingIndicatorSystem, pixelHoverSystem } from './systems' + +const canvasWidth = 16 +const canvasHeight = 16 + +const colors = [ + '#FFFFFF', // White + '#FFFF00', // Yellow + '#FF7F00', // Orange + '#FF0000', // Red + '#FF00FF', // Magenta + '#800080', // Purple + '#0000FF', // Blue + '#00FFFF', // Cyan + '#008000', // Green + '#006400', // Dark Green + '#8B4513', // Brown + '#D2B48C', // Tan + '#D3D3D3', // Light Grey + '#A9A9A9', // Medium Grey + '#696969', // Dark Grey + '#000000' // Black +] + +// This is the color player paints with +export let playersColor: string = colors[1] +export function updatePlayersColor(hexColor: string) { + playersColor = hexColor +} + +// Make canvas globaly accessable +export const canvas: Entity = createCanvas({ + position: { x: 4, y: 0.25, z: 8 }, + rotation: Quaternion.fromEulerDegrees(0, 0, 0), + scale: { x: 0.5, y: 0.5, z: 0.5 } +}) + +export async function main() { + // Load pixels from database + let pixelData = await getDatabase() + + // If there are no pixel, create new ones in database + if (pixelData.length == 0) { + // Writes empty pixels into databse + pixelData = await initDatabase(canvasWidth, canvasHeight, colors[15]) + } + console.log(pixelData) + + // Fill canvas with pixel from database + pixelData.forEach((pixel: { posX: number; posY: number; color: string; _id: string }) => { + createPixel(canvas, pixel.posX, pixel.posY, pixel.color, pixel._id) + }) + + // Create color picker + const colorPicker = createColorPicker({ + position: { x: 12, y: 1, z: 6 }, + rotation: Quaternion.fromEulerDegrees(0, 45, 0), + scale: { x: 0.25, y: 0.25, z: 0.25 } + }) + + // Fill color picker with colors + const width = 4 + const height = 4 + let index = 0 + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + createColor(colorPicker, x, y, colors[index]) + index++ + } + } + + // Start systems + engine.addSystem(pixelHoverSystem) + engine.addSystem(colorPickerHoverSystem) + engine.addSystem(loadingIndicatorSystem) +} diff --git a/Pixel-Canvas-API/src/messageBus.ts b/Pixel-Canvas-API/src/messageBus.ts new file mode 100644 index 000000000..978e07c59 --- /dev/null +++ b/Pixel-Canvas-API/src/messageBus.ts @@ -0,0 +1,36 @@ +import { Material, Transform, engine } from '@dcl/sdk/ecs' +import { MessageBus } from '@dcl/sdk/message-bus' +import { HexColor, Pixel } from './components' +import { Color3, Color4 } from '@dcl/sdk/math' +import { createLoadingIndicator } from './factory' + +// This is the global message bus for the whole scene +export const sceneMessageBus = new MessageBus() + +// Everytime you or another player places a pixel a message is send. +// So everyone can see the most recent pixels, +// without calling API all the time +sceneMessageBus.on('updatePixelColor', (newPixel) => { + console.log('MessageBus:') + console.log(newPixel) + // Get all entities with custom component Pixel + const pixels = engine.getEntitiesWith(Pixel) + for (const [entity] of pixels) { + const pos = Transform.getMutable(entity).position + if (pos.x == newPixel.posX && pos.y == newPixel.posY) { + // Change color of pixel + Material.setPbrMaterial(entity, { + albedoColor: Color4.fromHexString(newPixel.hexColor), + roughness: 0.5, + emissiveColor: Color3.fromHexString(newPixel.hexColor), + emissiveIntensity: 0.5 + }) + + // Update new color in custom component + HexColor.getMutable(entity).hexColor = newPixel.hexColor + + // This is a short visual and accoustic 'plop' animation + createLoadingIndicator(pos.x, pos.y, newPixel.hexColor, 'finished') + } + } +}) diff --git a/Pixel-Canvas-API/src/systems.ts b/Pixel-Canvas-API/src/systems.ts new file mode 100644 index 000000000..3ad11feee --- /dev/null +++ b/Pixel-Canvas-API/src/systems.ts @@ -0,0 +1,130 @@ +import { AudioSource, InputAction, Material, PointerEventType, Transform, engine, inputSystem } from '@dcl/sdk/ecs' +import { ColorPicker, HexColor, LoadingIndicator, Pixel } from './components' +import { Color3, Color4 } from '@dcl/sdk/math' +import { playersColor } from '.' + +// System tracking if player is pointing on a pixel entity +export function pixelHoverSystem() { + // Get Pixel Entities + const pixels = engine.getEntitiesWith(Pixel) + for (const [entity] of pixels) { + const hoverEnter = inputSystem.isTriggered(InputAction.IA_POINTER, PointerEventType.PET_HOVER_ENTER, entity) + const hoverLeave = inputSystem.isTriggered(InputAction.IA_POINTER, PointerEventType.PET_HOVER_LEAVE, entity) + + // If pointer hovers over this pixel entity + if (hoverEnter) { + // Color pixel with players color + Material.setPbrMaterial(entity, { + albedoColor: Color4.fromHexString(playersColor), + roughness: 0.5, + emissiveColor: Color3.fromHexString(playersColor), + emissiveIntensity: 0.5 + }) + + // Play sound + AudioSource.createOrReplace(entity, { + audioClipUrl: 'sounds/click_2.mp3', + playing: true + }) + } + + // If pointer leaves pixel + if (hoverLeave) { + // Color pixel back to it's original color + const originalColor = HexColor.get(entity).hexColor + Material.setPbrMaterial(entity, { + albedoColor: Color4.fromHexString(originalColor), + roughness: 0.5, + emissiveColor: Color3.fromHexString(originalColor), + emissiveIntensity: 0.5 + }) + } + } +} + +// System tracking if player points on a color picker entity +export function colorPickerHoverSystem() { + // Get ColorPicker Entities + const colorPickers = engine.getEntitiesWith(ColorPicker) + for (const [entity] of colorPickers) { + const hoverEnter = inputSystem.isTriggered(InputAction.IA_POINTER, PointerEventType.PET_HOVER_ENTER, entity) + const hoverLeave = inputSystem.isTriggered(InputAction.IA_POINTER, PointerEventType.PET_HOVER_LEAVE, entity) + + // If pointer hovers over pixel + if (hoverEnter) { + const pos = Transform.getMutable(entity).position + pos.z = -0.5 + + // Play sound + AudioSource.createOrReplace(entity, { + audioClipUrl: 'sounds/click_1.mp3', + playing: true + }) + } + + // If pointer leaves pixel + if (hoverLeave) { + const pos = Transform.getMutable(entity).position + pos.z = 0 + } + } +} + +// This System handles the bubble animation while pixel data loads +export function loadingIndicatorSystem() { + const indicators = engine.getEntitiesWith(LoadingIndicator) + for (const [entity] of indicators) { + let indicator = LoadingIndicator.getMutable(entity) + const scale = Transform.getMutable(entity).scale + const thisHexColor = HexColor.get(entity).hexColor + + if (indicator.status == 'loading') { + // If inidcator is bigger than a Pixel shrink it + if (scale.x > 1) { + scale.x = 0.1 + scale.y = 0.1 + scale.z = 0.1 + } else { + // Grow indicator + scale.x += 0.1 + scale.y += 0.1 + scale.z += 0.1 + } + } + + if (indicator.status == 'finished') { + // Reset indicator size to small + scale.x = 0.1 + scale.y = 0.1 + scale.z = 0.1 + + // Increase transparancy + let color = Color4.fromHexString(thisHexColor) + color.a = 0.25 + Material.setPbrMaterial(entity, { + albedoColor: color, + roughness: 0.5, + emissiveColor: Color3.fromHexString(thisHexColor), + emissiveIntensity: 0.5 + }) + indicator.status = 'finishedAnimation' + } + + if (indicator.status == 'finishedAnimation') { + // If animation is done remove indicator + if (scale.x > 2) { + engine.removeEntity(entity) + // Play sound + AudioSource.createOrReplace(entity, { + audioClipUrl: 'sounds/pop_2.mp3', + playing: true + }) + } else { + // Grow indicator + scale.x += 0.4 + scale.y += 0.4 + scale.z += 0.4 + } + } + } +} diff --git a/Pixel-Canvas-API/tsconfig.json b/Pixel-Canvas-API/tsconfig.json new file mode 100644 index 000000000..d84fa280f --- /dev/null +++ b/Pixel-Canvas-API/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "allowJs": true, + "strict": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "extends": "@dcl/sdk/types/tsconfig.ecs7.json" +} \ No newline at end of file diff --git a/dcl-workspace.json b/dcl-workspace.json index 281a34109..d6f792f81 100644 --- a/dcl-workspace.json +++ b/dcl-workspace.json @@ -162,6 +162,9 @@ { "path": "Portal-Puzzle" }, + { + "path": "Pixel-Canvas-API" + }, { "path": "Party-Time" }, diff --git a/package.json b/package.json index 90d94f67e..eaca62d8a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "Jukebox", "Laser-ray-Casting", "Party-Time", + "Pixel-Canvas-API", "Portal-Puzzle", "Puffer", "Random-noise-movement",