├── LICENSE ├── README.md └── sprite.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 PlayCanvas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | **This library is no longer supported. We recommend using the [Element Component](https://developer.playcanvas.com/en/user-manual/user-interface/) to render 2D images in PlayCanvas** 4 | 5 | --- 6 | 7 | Rendering Sprites in PlayCanvas 8 | ================================ 9 | 10 | Download sprite.js and upload it to your project or copy paste the code in a new script. 11 | 12 | Add the script to an Entity with a Script Component. You will see the following script attributes: 13 | 14 | - **textureAsset**: This is the sprite that you want to render. If you want transparency for your sprite make sure that this is a .png file. 15 | - **pos**: This is the Vec2 (x,y) **screen** coordinate for your sprite. 16 | - **size**: This is the width and height as Vec2 of your sprite in pixels. For best results use the actual width of the uploaded image. Powers of 2 have better quality. 17 | - **depth**: This is the z-index of your sprite. If you want it to appear behind other sprites increase this value. 18 | - **uPercentage**: A value between [0,1] that specifies the maximum u value of the texture. 19 | - **vPercentage**: A value between [0,1] that specifies the maximum v value of the texture. 20 | - **anchor**: Determines where to anchor the sprite on the screen, for example top, center, bottom right etc. 21 | - **pivot**: Determines the alignment (or pivot point) of the sprite. 22 | - **tint**: A color to multiply the current color of the sprite with. 23 | - **maxResHeight**: Set this to the target resolution height of your app. The final scale of your sprite will be calculated as canvasHeight / maxResHeight. 24 | 25 | Creating a UI with Sprites 26 | =========================== 27 | 28 | Using the sprite.js script you can create a user interface for your application. 29 | 30 | - Create images using your favorite tool. For best quality sprites should be png files with power of 2 width and height. 31 | - Upload them to PlayCanvas. 32 | 33 | For each one of your sprites: 34 | - Create an Entity 35 | - Add a script component to the Entity 36 | - Add sprite to script component 37 | - Click on textureAsset and pick the desired image 38 | - Set the rest of the fields to your liking 39 | - Launch the application 40 | 41 | Buttons 42 | ======= 43 | 44 | You can attach an event handler for each sprite for the 'click' event. For example add this script on the same Entity as the sprite: 45 | 46 | ``` 47 | var MyScript = pc.createScript('myScript'); 48 | 49 | MyScript.prototype.initialize = function() { 50 | this.entity.script.sprite.on('click', this.onClick, this); 51 | }; 52 | 53 | MyScript.prototype.onClick = function() { 54 | console.log('click'); 55 | }; 56 | ``` 57 | 58 | That way you can have buttons that do something when you click on them. 59 | 60 | Progress Bars 61 | ============= 62 | 63 | Check out this scene for an example on how to make progress bars using sprites and different anchors: 64 | 65 | https://playcanvas.com/editor/scene/443447 66 | 67 | Rendering Text 68 | ============== 69 | 70 | Check out this repository for scripts and details on how to render bitmap fonts: https://github.com/playcanvas/fonts 71 | -------------------------------------------------------------------------------- /sprite.js: -------------------------------------------------------------------------------- 1 | var Sprite = pc.createScript('sprite'); 2 | 3 | /** 4 | * Attributes 5 | */ 6 | 7 | Sprite.attributes.add('textureAsset', { 8 | type: 'asset', 9 | assetType: 'texture', 10 | description: 'The sprite texture' 11 | }); 12 | 13 | Sprite.attributes.add('pos', { 14 | type: 'vec2', 15 | description: 'The coordinate in pixels', 16 | placeholder: [ 'x', 'y' ] 17 | }); 18 | 19 | Sprite.attributes.add('size', { 20 | type: 'vec2', 21 | description: 'The size of sprite in pixels', 22 | placeholder: [ 'w', 'h' ], 23 | default: [ 128, 128 ] 24 | }); 25 | 26 | Sprite.attributes.add('depth', { 27 | type: 'number', 28 | description: 'The z depth of the sprite compared to other sprites', 29 | default: 1 30 | }); 31 | 32 | Sprite.attributes.add('uPercentage', { 33 | type: 'number', 34 | description: 'The horizontal texture percentage that is visible (used for progress bars)', 35 | default: 1, 36 | min: 0, 37 | max: 1 38 | }); 39 | 40 | Sprite.attributes.add('vPercentage', { 41 | type: 'number', 42 | description: 'The vertical texture percentage that is visible (used for progress bars)', 43 | default: 1, 44 | min: 0, 45 | max: 1 46 | }); 47 | 48 | Sprite.attributes.add('anchor', { 49 | type: 'number', 50 | default: 0, 51 | description: 'The anchor of the sprite related to the screen bounds', 52 | enum: [ 53 | {'topLeft': 0}, 54 | {'top': 1}, 55 | {'topRight': 2}, 56 | {'left': 3}, 57 | {'center': 4}, 58 | {'right': 5}, 59 | {'bottomLeft': 6}, 60 | {'bottom': 7}, 61 | {'bottomRight': 8} 62 | ] 63 | }); 64 | 65 | Sprite.attributes.add('pivot', { 66 | type: 'number', 67 | default: 0, 68 | description: 'The pivot point of the sprite', 69 | enum: [ 70 | {'topLeft': 0}, 71 | {'top': 1}, 72 | {'topRight': 2}, 73 | {'left': 3}, 74 | {'center': 4}, 75 | {'right': 5}, 76 | {'bottomLeft': 6}, 77 | {'bottom': 7}, 78 | {'bottomRight': 8} 79 | ] 80 | }); 81 | 82 | 83 | Sprite.attributes.add('tint', { 84 | type: 'rgba', 85 | description: 'A color that is multiplied with the current color of the sprite', 86 | default: [1,1,1,1] 87 | }); 88 | 89 | Sprite.attributes.add('maxResHeight', { 90 | type: 'number', 91 | default: 720, 92 | description: 'The maximum resolution height of the application. Used to scale the sprite accordingly.' 93 | }); 94 | 95 | /** 96 | * Static variables 97 | */ 98 | Sprite.material = null; 99 | Sprite.vertexFormat = null; 100 | Sprite.resolution = new pc.Vec2(); 101 | 102 | 103 | // initialize code called once per entity 104 | Sprite.prototype.initialize = function() { 105 | var canvas = document.getElementById('application-canvas'); 106 | 107 | this.userOffset = new pc.Vec2(); 108 | this.offset = new pc.Vec2(); 109 | this.scaling = new pc.Vec2(); 110 | this.anchorOffset = new pc.Vec2(); 111 | this.pivotOffset = new pc.Vec2(); 112 | 113 | var app = this.app; 114 | 115 | // Create shader 116 | var gd = app.graphicsDevice; 117 | 118 | // Create material and shader 119 | if (! Sprite.material) { 120 | var shaderDefinition = { 121 | attributes: { 122 | aPosition: pc.SEMANTIC_POSITION, 123 | aUv0: pc.SEMANTIC_TEXCOORD0 124 | }, 125 | vshader: [ 126 | "attribute vec2 aPosition;", 127 | "attribute vec2 aUv0;", 128 | "varying vec2 vUv0;", 129 | "uniform vec2 uResolution;", 130 | "uniform vec2 uOffset;", 131 | "uniform vec2 uScale;", 132 | "", 133 | "void main(void)", 134 | "{", 135 | " gl_Position = vec4(2.0 * ((uScale * aPosition.xy + uOffset) / uResolution ) - 1.0, -0.9, 1.0);", 136 | " vUv0 = aUv0;", 137 | "}" 138 | ].join("\n"), 139 | fshader: [ 140 | "precision " + gd.precision + " float;", 141 | "", 142 | "varying vec2 vUv0;", 143 | "", 144 | "uniform vec4 vTint;", 145 | "", 146 | "uniform sampler2D uColorMap;", 147 | "", 148 | "void main(void)", 149 | "{", 150 | " vec4 color = texture2D(uColorMap, vUv0);", 151 | " gl_FragColor = vec4(color.rgb * vTint.rgb, color.a * vTint.a);", 152 | "}" 153 | ].join("\n") 154 | }; 155 | 156 | var shader = new pc.Shader(gd, shaderDefinition); 157 | 158 | var material = new pc.Material(); 159 | material.shader = shader; 160 | material.blend = true; 161 | material.blendSrc = pc.BLENDMODE_SRC_ALPHA; 162 | material.blendDst = pc.BLENDMODE_ONE_MINUS_SRC_ALPHA; 163 | material.depthTest = false; 164 | material.depthWrite = false; 165 | Sprite.material = material; 166 | } 167 | 168 | // Create the vertex format 169 | if (!Sprite.vertexFormat) { 170 | Sprite.vertexFormat = new pc.VertexFormat(gd, [ 171 | { semantic: pc.SEMANTIC_POSITION, components: 2, type: pc.ELEMENTTYPE_FLOAT32 }, 172 | { semantic: pc.SEMANTIC_TEXCOORD0, components: 2, type: pc.ELEMENTTYPE_FLOAT32 } 173 | ]); 174 | } 175 | 176 | // Create mesh 177 | var mesh = new pc.Mesh(); 178 | mesh.vertexBuffer = new pc.VertexBuffer(gd, Sprite.vertexFormat, 6, pc.BUFFER_DYNAMIC); 179 | mesh.primitive[0].type = pc.PRIMITIVE_TRIANGLES; 180 | mesh.primitive[0].base = 0; 181 | mesh.primitive[0].count = 6; 182 | mesh.primitive[0].indexed = false; 183 | 184 | // Create mesh instance 185 | this.meshInstance = new pc.MeshInstance(this.entity, mesh, Sprite.material); 186 | this.meshInstance.castShadow = false; 187 | this.meshInstance.receiveShadow = false; 188 | this.meshInstance.drawOrder = this.depth; 189 | 190 | this.texture = this.textureAsset.resource; 191 | 192 | // Create a vertex buffer 193 | this.updateSprite(); 194 | 195 | // Get layer 196 | this.layer = app.scene.layers.getLayerById(pc.LAYERID_UI) || app.scene.layers.getLayerById(pc.LAYERID_WORLD); 197 | 198 | app.mouse.on('mousedown', this.onMouseDown, this); 199 | if (app.touch) { 200 | app.touch.on('touchstart', this.onTouchDown, this); 201 | } 202 | 203 | this.on('state', this.onState); 204 | this.on('destroy', this.onDestroy, this); 205 | this.on('attr:depth', function(value) { 206 | this.eventsEnabled = false; 207 | this.meshInstance.drawOrder = value; 208 | }); 209 | this.on('attr:size', this.updateSprite); 210 | this.on('attr:uPercentage', this.updateSprite); 211 | this.on('attr:vPercentage', this.updateSprite); 212 | 213 | this.onState(true); 214 | }; 215 | 216 | Sprite.prototype.onMouseDown = function (e) { 217 | if (!this.eventsEnabled) { 218 | return; 219 | } 220 | 221 | this.onClick(e); 222 | }; 223 | 224 | Sprite.prototype.onTouchDown = function (e) { 225 | if (!this.eventsEnabled) { 226 | return; 227 | } 228 | 229 | this.onClick(e.changedTouches[0]); 230 | }; 231 | 232 | /** 233 | * Calculates if the click has happened inside the rect of this 234 | * sprite and fires 'click' event if it has 235 | */ 236 | Sprite.prototype.onClick = function (cursor) { 237 | var canvas = this.app.graphicsDevice.canvas; 238 | var tlx, tly, brx, bry, mx, my; 239 | 240 | 241 | var scaling = this.scaling; 242 | var offset = this.offset; 243 | 244 | tlx = 2.0 * (scaling.x * 0 + offset.x) / Sprite.resolution.x - 1.0; 245 | tly = 2.0 * (scaling.y * 0 + offset.y) / Sprite.resolution.y - 1.0; 246 | 247 | 248 | brx = 2.0 * (scaling.x * this.size.x + offset.x) / Sprite.resolution.x - 1.0; 249 | bry = 2.0 * (scaling.y * (- this.size.y) + offset.y) / Sprite.resolution.y - 1.0; 250 | 251 | mx = (2.0 * cursor.x / canvas.offsetWidth) - 1; 252 | my = (2.0 * (canvas.offsetHeight - cursor.y) / canvas.offsetHeight) - 1; 253 | 254 | if (mx >= tlx && mx <= brx && 255 | my <= tly && my >= bry) { 256 | this.fire('click'); 257 | } 258 | }; 259 | 260 | Sprite.prototype.updateSprite = function () { 261 | if (!this.meshInstance) { 262 | return; 263 | } 264 | 265 | this.eventsEnabled = false; 266 | 267 | // Fill the vertex buffer 268 | var vb = this.meshInstance.mesh.vertexBuffer; 269 | vb.lock(); 270 | 271 | var canvas = this.app.graphicsDevice.canvas; 272 | 273 | // Add vertices 274 | var iterator = new pc.VertexIterator(vb); 275 | iterator.element[pc.SEMANTIC_POSITION].set(0, -this.size.y); 276 | iterator.element[pc.SEMANTIC_TEXCOORD0].set(0, 0); 277 | iterator.next(); 278 | iterator.element[pc.SEMANTIC_POSITION].set(this.size.x, -this.size.y); 279 | iterator.element[pc.SEMANTIC_TEXCOORD0].set(this.uPercentage, 0); 280 | iterator.next(); 281 | iterator.element[pc.SEMANTIC_POSITION].set(0, 0); 282 | iterator.element[pc.SEMANTIC_TEXCOORD0].set(0, this.vPercentage); 283 | iterator.next(); 284 | iterator.element[pc.SEMANTIC_POSITION].set(this.size.x, -this.size.y); 285 | iterator.element[pc.SEMANTIC_TEXCOORD0].set(this.uPercentage, 0); 286 | iterator.next(); 287 | iterator.element[pc.SEMANTIC_POSITION].set(this.size.x, 0); 288 | iterator.element[pc.SEMANTIC_TEXCOORD0].set(this.uPercentage, this.vPercentage); 289 | iterator.next(); 290 | iterator.element[pc.SEMANTIC_POSITION].set(0, 0); 291 | iterator.element[pc.SEMANTIC_TEXCOORD0].set(0, this.vPercentage); 292 | 293 | vb.unlock(); 294 | }; 295 | 296 | Sprite.prototype.calculateOffset = function () { 297 | var canvas = this.app.graphicsDevice.canvas; 298 | this.calculateAnchorOffset(); 299 | this.calculatePivotOffset(); 300 | 301 | this.offset.set(this.pos.x * this.scaling.x, this.pos.y * this.scaling.y) 302 | .add(this.userOffset) 303 | .add(this.anchorOffset) 304 | .add(this.pivotOffset); 305 | 306 | this.offset.y += canvas.offsetHeight; 307 | return this.offset; 308 | }; 309 | 310 | Sprite.prototype.calculateScaling = function () { 311 | var canvas = this.app.graphicsDevice.canvas; 312 | var scale = canvas.offsetHeight / this.maxResHeight; 313 | this.scaling.set(scale, scale); 314 | return this.scaling; 315 | }; 316 | 317 | Sprite.prototype.calculateAnchorOffset = function () { 318 | var canvas = this.app.graphicsDevice.canvas; 319 | var width = canvas.offsetWidth; 320 | var height = canvas.offsetHeight; 321 | 322 | switch (this.anchor) { 323 | // top left 324 | case 0: 325 | this.anchorOffset.set(0,0); 326 | break; 327 | // top 328 | case 1: 329 | this.anchorOffset.set(width * 0.5, 0); 330 | break; 331 | // top right 332 | case 2: 333 | this.anchorOffset.set(width, 0); 334 | break; 335 | // left 336 | case 3: 337 | this.anchorOffset.set(0, -height * 0.5); 338 | break; 339 | // center 340 | case 4: 341 | this.anchorOffset.set(width * 0.5, -height * 0.5); 342 | break; 343 | // right 344 | case 5: 345 | this.anchorOffset.set(width, -height * 0.5); 346 | break; 347 | // bottom left 348 | case 6: 349 | this.anchorOffset.set(0, -height); 350 | break; 351 | // bottom 352 | case 7: 353 | this.anchorOffset.set(width/2, -height); 354 | break; 355 | // bottom right 356 | case 8: 357 | this.anchorOffset.set(width, -height); 358 | break; 359 | default: 360 | console.error('Wrong anchor: ' + this.anchor); 361 | break; 362 | } 363 | 364 | return this.anchorOffset; 365 | }; 366 | 367 | Sprite.prototype.calculatePivotOffset = function () { 368 | var width = this.size.x * this.scaling.x; 369 | var height = this.size.y * this.scaling.y; 370 | 371 | switch (this.pivot) { 372 | // top left 373 | case 0: 374 | this.pivotOffset.set(0,0); 375 | break; 376 | // top 377 | case 1: 378 | this.pivotOffset.set(-width * 0.5, 0); 379 | break; 380 | // top right 381 | case 2: 382 | this.pivotOffset.set(-width, 0); 383 | break; 384 | // left 385 | case 3: 386 | this.pivotOffset.set(0, height * 0.5); 387 | break; 388 | // center 389 | case 4: 390 | this.pivotOffset.set(-width * 0.5, height * 0.5); 391 | break; 392 | // right 393 | case 5: 394 | this.pivotOffset.set(-width, height * 0.5); 395 | break; 396 | // bottom left 397 | case 6: 398 | this.pivotOffset.set(0, height); 399 | break; 400 | // bottom 401 | case 7: 402 | this.pivotOffset.set(-width/2, height); 403 | break; 404 | // bottom right 405 | case 8: 406 | this.pivotOffset.set(-width, height); 407 | break; 408 | default: 409 | console.error('Wrong pivot: ' + this.pivot); 410 | break; 411 | } 412 | 413 | return this.pivotOffset; 414 | }; 415 | 416 | Sprite.prototype.onState = function(enabled) { 417 | this.eventsEnabled = false; 418 | 419 | if (this.layer) { 420 | if (enabled) { 421 | this.layer.addMeshInstances([this.meshInstance]); 422 | } else { 423 | this.layer.removeMeshInstances([this.meshInstance]); 424 | } 425 | } 426 | 427 | }; 428 | 429 | Sprite.prototype.update = function (dt) { 430 | this.eventsEnabled = true; 431 | var canvas = this.app.graphicsDevice.canvas; 432 | Sprite.resolution.set(canvas.offsetWidth, canvas.offsetHeight); 433 | this.meshInstance.setParameter('uResolution', Sprite.resolution.data); 434 | this.meshInstance.setParameter('uScale', this.calculateScaling().data); 435 | this.meshInstance.setParameter('uOffset', this.calculateOffset().data); 436 | this.meshInstance.setParameter('uColorMap', this.texture); 437 | this.meshInstance.setParameter('vTint', this.tint.data); 438 | }; 439 | 440 | Sprite.prototype.onDestroy = function () { 441 | // remove mesh instance 442 | if (this.layer) { 443 | this.layer.removeMeshInstances([this.meshInstance]); 444 | } 445 | }; 446 | --------------------------------------------------------------------------------