Draw a squirrel
This change introduces the Drawable class, which corresponds to a Scratch sprite or clone. It supports setting its "skin" (corresponding to a Scratch costume) by md5+extension, but currently only supports bitmap skins. Drawables can be created, destroyed, and otherwise manipulated by ID through the renderer.
This commit is contained in:
@@ -11,11 +11,23 @@
|
|||||||
<script>
|
<script>
|
||||||
var canvas = document.getElementById('scratch-stage');
|
var canvas = document.getElementById('scratch-stage');
|
||||||
var renderer = new RenderWebGL(canvas);
|
var renderer = new RenderWebGL(canvas);
|
||||||
|
var drawableID = renderer.createDrawable();
|
||||||
|
|
||||||
function step() {
|
function drawStep() {
|
||||||
renderer.draw();
|
renderer.draw();
|
||||||
requestAnimationFrame(step);
|
requestAnimationFrame(drawStep);
|
||||||
}
|
}
|
||||||
step();
|
var direction = 90;
|
||||||
|
var posX = 0;
|
||||||
|
var posY = 0;
|
||||||
|
var scale = 100;
|
||||||
|
function thinkStep() {
|
||||||
|
direction += 0.1;
|
||||||
|
renderer.setDrawablePosition(drawableID, posX, posY);
|
||||||
|
renderer.setDrawableDirection(drawableID, direction);
|
||||||
|
renderer.setDrawableScale(drawableID, scale);
|
||||||
|
}
|
||||||
|
drawStep();
|
||||||
|
setInterval(thinkStep, 1/60);
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
150
src/Drawable.js
Normal file
150
src/Drawable.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
var twgl = require('twgl.js');
|
||||||
|
|
||||||
|
function Drawable(renderer, gl) {
|
||||||
|
this._id = Drawable._nextDrawable++;
|
||||||
|
Drawable._allDrawables[this._id] = this;
|
||||||
|
|
||||||
|
this._renderer = renderer;
|
||||||
|
this._gl = gl;
|
||||||
|
|
||||||
|
// TODO: double-buffer uniforms
|
||||||
|
this._uniforms = {
|
||||||
|
u_texture: null,
|
||||||
|
u_mvp: twgl.m4.identity(),
|
||||||
|
u_brightness_shift: 0,
|
||||||
|
u_hue_shift: 0,
|
||||||
|
u_whirl_radians: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
this._position = twgl.v3.create(0, 0);
|
||||||
|
this._scale = 100;
|
||||||
|
this._direction = 90;
|
||||||
|
this._dimensions = twgl.v3.create(0, 0);
|
||||||
|
this._transformDirty = true;
|
||||||
|
this._costumeResolution = 2; // TODO: only for bitmaps
|
||||||
|
|
||||||
|
this.setSkin(this._DEFAULT_SKIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Drawable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID to be assigned next time the Drawable constructor is called.
|
||||||
|
* @type {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
Drawable._nextDrawable = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All current Drawables, by ID.
|
||||||
|
* @type {Object.<int, Drawable>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
Drawable._allDrawables = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a Drawable by its ID number.
|
||||||
|
* @param drawableID {int} The ID of the Drawable to fetch.
|
||||||
|
* @returns {?Drawable} The specified Drawable if found, otherwise null.
|
||||||
|
*/
|
||||||
|
Drawable.getDrawableByID = function (drawableID) {
|
||||||
|
return Drawable._allDrawables[drawableID];
|
||||||
|
};
|
||||||
|
|
||||||
|
Drawable.dirtyAllTransforms = function () {
|
||||||
|
for (var drawableID in Drawable._allDrawables) {
|
||||||
|
if (Drawable._allDrawables.hasOwnProperty(drawableID)) {
|
||||||
|
var drawable = Drawable._allDrawables[drawableID];
|
||||||
|
drawable.setTransformDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: fall back on a built-in skin to protect against network problems
|
||||||
|
Drawable.prototype._DEFAULT_SKIN = {
|
||||||
|
squirrel: '7e24c99c1b853e52f8e7f9004416fa34.png',
|
||||||
|
bus: '66895930177178ea01d9e610917f8acf.png'
|
||||||
|
}.squirrel;
|
||||||
|
|
||||||
|
Drawable.prototype.dispose = function () {
|
||||||
|
this.setSkin(null);
|
||||||
|
if (this._id >= 0) {
|
||||||
|
delete Drawable[this._id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Drawable.prototype.setTransformDirty = function () {
|
||||||
|
this._transformDirty = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Drawable.prototype.getID = function () {
|
||||||
|
return this._id;
|
||||||
|
};
|
||||||
|
|
||||||
|
Drawable.prototype.setSkin = function (skin_md5ext) {
|
||||||
|
// TODO: share Skins across Drawables - see also destroy()
|
||||||
|
if (this._uniforms.u_texture) {
|
||||||
|
this._gl.deleteTexture(this._uniforms);
|
||||||
|
}
|
||||||
|
if (skin_md5ext) {
|
||||||
|
var url =
|
||||||
|
'https://cdn.assets.scratch.mit.edu/internalapi/asset/' +
|
||||||
|
skin_md5ext +
|
||||||
|
'/get/';
|
||||||
|
var instance = this;
|
||||||
|
this._uniforms.u_texture =
|
||||||
|
twgl.createTexture(this._gl, {
|
||||||
|
auto: true,
|
||||||
|
src: url
|
||||||
|
}, function (err, texture, source) {
|
||||||
|
if (!err) {
|
||||||
|
instance._dimensions[0] = source.width;
|
||||||
|
instance._dimensions[1] = source.height;
|
||||||
|
instance.setTransformDirty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._uniforms.u_texture = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Drawable.prototype.getUniforms = function () {
|
||||||
|
if (this._transformDirty) {
|
||||||
|
this._calculateTransform();
|
||||||
|
}
|
||||||
|
return this._uniforms;
|
||||||
|
};
|
||||||
|
|
||||||
|
Drawable.prototype.setPosition = function (x, y) {
|
||||||
|
if (this._position[0] != x || this._position[1] != y) {
|
||||||
|
this._position[0] = x;
|
||||||
|
this._position[1] = y;
|
||||||
|
this.setTransformDirty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Drawable.prototype.setDirection = function (directionDegrees) {
|
||||||
|
if (this._direction != directionDegrees) {
|
||||||
|
this._direction = directionDegrees;
|
||||||
|
this.setTransformDirty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Drawable.prototype.setScale = function (scalePercent) {
|
||||||
|
if(this._scale != scalePercent) {
|
||||||
|
this._scale = scalePercent;
|
||||||
|
this.setTransformDirty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Drawable.prototype._calculateTransform = function () {
|
||||||
|
var rotation = (270 - this._direction) * Math.PI / 180;
|
||||||
|
var scale = this._scale / 100 / this._costumeResolution;
|
||||||
|
var projection = this._renderer.getProjectionMatrix();
|
||||||
|
var mvp = this._uniforms.u_mvp;
|
||||||
|
twgl.m4.translate(projection, this._position, mvp);
|
||||||
|
twgl.m4.rotateZ(mvp, rotation, mvp);
|
||||||
|
twgl.m4.scale(mvp, twgl.v3.mulScalar(this._dimensions, scale), mvp);
|
||||||
|
this._transformDirty = false;
|
||||||
|
};
|
||||||
105
src/index.js
105
src/index.js
@@ -2,6 +2,8 @@ var EventEmitter = require('events');
|
|||||||
var twgl = require('twgl.js');
|
var twgl = require('twgl.js');
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
|
|
||||||
|
var Drawable = require('./drawable');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a renderer for drawing Scratch sprites to a canvas using WebGL.
|
* Create a renderer for drawing Scratch sprites to a canvas using WebGL.
|
||||||
* Optionally, specify the logical and/or physical size of the Scratch stage.
|
* Optionally, specify the logical and/or physical size of the Scratch stage.
|
||||||
@@ -24,9 +26,12 @@ function RenderWebGL(
|
|||||||
// Bind event emitter and runtime to VM instance
|
// Bind event emitter and runtime to VM instance
|
||||||
EventEmitter.call(this);
|
EventEmitter.call(this);
|
||||||
|
|
||||||
|
// TODO: remove?
|
||||||
|
twgl.setDefaults({crossOrigin: true});
|
||||||
|
|
||||||
this._gl = twgl.getWebGLContext(canvas);
|
this._gl = twgl.getWebGLContext(canvas);
|
||||||
this._drawables = {};
|
this._drawables = [];
|
||||||
this._uniforms = {};
|
this._projection = twgl.m4.identity();
|
||||||
|
|
||||||
this._createPrograms();
|
this._createPrograms();
|
||||||
this._createGeometry();
|
this._createGeometry();
|
||||||
@@ -61,8 +66,8 @@ RenderWebGL.prototype.setStageSize = function (xLeft, xRight, yBottom, yTop) {
|
|||||||
this._xRight = xRight;
|
this._xRight = xRight;
|
||||||
this._yBottom = yBottom;
|
this._yBottom = yBottom;
|
||||||
this._yTop = yTop;
|
this._yTop = yTop;
|
||||||
this._uniforms.u_projection =
|
this._projection = twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1);
|
||||||
twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1);
|
Drawable.dirtyAllTransforms();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,18 +92,70 @@ RenderWebGL.prototype.draw = function () {
|
|||||||
gl.clearColor(1, 0, 1, 1);
|
gl.clearColor(1, 0, 1, 1);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
gl.enable(gl.BLEND);
|
||||||
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
gl.useProgram(this._programInfo.program);
|
gl.useProgram(this._programInfo.program);
|
||||||
twgl.setBuffersAndAttributes(gl, this._programInfo, this._bufferInfo);
|
twgl.setBuffersAndAttributes(gl, this._programInfo, this._bufferInfo);
|
||||||
|
|
||||||
twgl.setUniforms(this._programInfo, this._uniforms);
|
var numDrawables = this._drawables.length;
|
||||||
|
for (var drawableIndex = 0; drawableIndex < numDrawables; ++drawableIndex) {
|
||||||
for (var id in this._drawables) {
|
var drawableID = this._drawables[drawableIndex];
|
||||||
if (this._drawables.hasOwnProperty(id)) {
|
var drawable = Drawable.getDrawableByID(drawableID);
|
||||||
var drawable = this._drawables[id];
|
twgl.setUniforms(this._programInfo, drawable.getUniforms());
|
||||||
twgl.setUniforms(this._programInfo, drawable);
|
|
||||||
twgl.drawBufferInfo(gl, gl.TRIANGLES, this._bufferInfo);
|
twgl.drawBufferInfo(gl, gl.TRIANGLES, this._bufferInfo);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Drawable and add it to the scene.
|
||||||
|
* @returns {int} The ID of the new Drawable.
|
||||||
|
*/
|
||||||
|
RenderWebGL.prototype.createDrawable = function () {
|
||||||
|
var drawable = new Drawable(this, this._gl);
|
||||||
|
var drawableID = drawable.getID();
|
||||||
|
this._drawables.push(drawableID);
|
||||||
|
return drawableID;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy a Drawable, removing it from the scene.
|
||||||
|
* @param {int} drawableID The ID of the Drawable to remove.
|
||||||
|
* @returns {boolean} True iff the drawable was found and removed.
|
||||||
|
*/
|
||||||
|
RenderWebGL.prototype.destroyDrawable = function (drawableID) {
|
||||||
|
var index = this._drawables.indexOf(drawableID);
|
||||||
|
if (index >= 0) {
|
||||||
|
Drawable.getDrawableByID(drawableID).dispose();
|
||||||
|
this._drawables.splice(index, 1);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderWebGL.prototype.setDrawablePosition = function (drawableID, x, y) {
|
||||||
|
var drawable = Drawable.getDrawableByID(drawableID);
|
||||||
|
if (drawable) {
|
||||||
|
drawable.setPosition(x, y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderWebGL.prototype.setDrawableDirection = function (drawableID, directionDegrees) {
|
||||||
|
var drawable = Drawable.getDrawableByID(drawableID);
|
||||||
|
if (drawable) {
|
||||||
|
drawable.setDirection(directionDegrees);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderWebGL.prototype.setDrawableScale = function (drawableID, scalePercent) {
|
||||||
|
var drawable = Drawable.getDrawableByID(drawableID);
|
||||||
|
if (drawable) {
|
||||||
|
drawable.setScale(scalePercent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderWebGL.prototype.getProjectionMatrix = function () {
|
||||||
|
return this._projection;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,14 +175,28 @@ RenderWebGL.prototype._createPrograms = function () {
|
|||||||
*/
|
*/
|
||||||
RenderWebGL.prototype._createGeometry = function () {
|
RenderWebGL.prototype._createGeometry = function () {
|
||||||
var quad = {
|
var quad = {
|
||||||
position: [
|
a_position: {
|
||||||
-0.5, -0.5, 0,
|
numComponents: 2,
|
||||||
0.5, -0.5, 0,
|
data: [
|
||||||
-0.5, 0.5, 0,
|
-0.5, -0.5,
|
||||||
-0.5, 0.5, 0,
|
0.5, -0.5,
|
||||||
0.5, -0.5, 0,
|
-0.5, 0.5,
|
||||||
0.5, 0.5, 0
|
-0.5, 0.5,
|
||||||
|
0.5, -0.5,
|
||||||
|
0.5, 0.5
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
a_texCoord: {
|
||||||
|
numComponents: 2,
|
||||||
|
data: [
|
||||||
|
1, 0,
|
||||||
|
0, 0,
|
||||||
|
1, 1,
|
||||||
|
1, 1,
|
||||||
|
0, 0,
|
||||||
|
0, 1
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
this._bufferInfo = twgl.createBufferInfoFromArrays(this._gl, quad);
|
this._bufferInfo = twgl.createBufferInfoFromArrays(this._gl, quad);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
precision mediump float;
|
precision mediump float;
|
||||||
|
|
||||||
uniform sampler2D u_image;
|
uniform sampler2D u_skin;
|
||||||
varying vec2 v_texCoord;
|
|
||||||
|
|
||||||
uniform float u_hue_shift;
|
|
||||||
uniform float u_brightness_shift;
|
uniform float u_brightness_shift;
|
||||||
|
uniform float u_hue_shift;
|
||||||
uniform float u_whirl_radians;
|
uniform float u_whirl_radians;
|
||||||
|
|
||||||
|
varying vec2 v_texCoord;
|
||||||
|
|
||||||
vec3 convertRGB2HSV(vec3 rgb)
|
vec3 convertRGB2HSV(vec3 rgb)
|
||||||
{
|
{
|
||||||
float maxRGB = max(max(rgb.r, rgb.g), rgb.b);
|
float maxRGB = max(max(rgb.r, rgb.g), rgb.b);
|
||||||
@@ -98,7 +97,7 @@ void main()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gl_FragColor = texture2D(u_image, texcoord0);
|
gl_FragColor = texture2D(u_skin, texcoord0);
|
||||||
|
|
||||||
// TODO: See if we can/should use actual alpha test.
|
// TODO: See if we can/should use actual alpha test.
|
||||||
// Does bgfx offer a way to set u_alphaRef? Would that help?
|
// Does bgfx offer a way to set u_alphaRef? Would that help?
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
attribute vec4 position;
|
uniform mat4 u_mvp;
|
||||||
|
|
||||||
|
attribute vec2 a_position;
|
||||||
|
attribute vec2 a_texCoord;
|
||||||
|
|
||||||
varying vec2 v_texCoord;
|
varying vec2 v_texCoord;
|
||||||
|
|
||||||
uniform mat4 u_transform;
|
|
||||||
uniform mat4 u_projection;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = u_projection * u_transform * position;
|
gl_Position = u_mvp * vec4(a_position, 0, 1);
|
||||||
|
v_texCoord = a_texCoord;
|
||||||
// Map clipspace coordinates to texture coordinates
|
|
||||||
v_texCoord = (position.xy * vec2(1.0, -1.0)) + vec2(0.5);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user