mirror of
https://github.com/scratchfoundation/scratch-html5.git
synced 2024-11-28 18:16:11 -05:00
466 lines
16 KiB
JavaScript
466 lines
16 KiB
JavaScript
// Copyright (C) 2013 Massachusetts Institute of Technology
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License version 2,
|
|
// as published by the Free Software Foundation.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
// Scratch HTML5 Player
|
|
// Sprite.js
|
|
// Tim Mickel, July 2011 - March 2012
|
|
|
|
// The Sprite provides the interface and implementation for Scratch sprite-control
|
|
|
|
'use strict';
|
|
|
|
var Sprite = function(data) {
|
|
if (!this.data) {
|
|
this.data = data;
|
|
}
|
|
|
|
// Public variables used for Scratch-accessible data.
|
|
this.visible = typeof(this.data.visible) == "undefined" ? true : data.visible;
|
|
|
|
this.scratchX = data.scratchX || 0;
|
|
this.scratchY = data.scratchY || 0;
|
|
|
|
this.scale = data.scale || 1.0;
|
|
|
|
this.direction = data.direction || 90;
|
|
this.rotation = (data.direction - 90) || 0;
|
|
this.rotationStyle = data.rotationStyle || 'normal';
|
|
this.isFlipped = data.direction < 0 && data.rotationStyle == 'leftRight';
|
|
this.costumes = data.costumes || [];
|
|
this.currentCostumeIndex = data.currentCostumeIndex || 0;
|
|
this.previousCostumeIndex = -1;
|
|
|
|
this.objName = data.objName || '';
|
|
|
|
this.variables = {};
|
|
if (data.variables) {
|
|
for (var i = 0; i < data.variables.length; i++) {
|
|
this.variables[data.variables[i]['name']] = data.variables[i]['value'];
|
|
}
|
|
}
|
|
this.lists = {};
|
|
if (data.lists) {
|
|
for (var i = 0; i < data.lists.length; i++) {
|
|
this.lists[data.lists[i]['listName']] = data.lists[i];
|
|
}
|
|
}
|
|
|
|
// Used for the pen
|
|
this.penIsDown = false;
|
|
this.penWidth = 1;
|
|
this.penHue = 120; // blue
|
|
this.penShade = 50; // full brightness and saturation
|
|
this.penColorCache = 0x0000FF;
|
|
|
|
// Used for layering
|
|
if (!this.z) this.z = io.getCount();
|
|
|
|
// HTML element for the talk bubbles
|
|
this.talkBubble = null;
|
|
this.talkBubbleBox = null;
|
|
this.talkBubbleStyler = null;
|
|
this.talkBubbleOn = false;
|
|
|
|
// Internal variables used for rendering meshes.
|
|
this.textures = [];
|
|
this.materials = [];
|
|
this.geometries = [];
|
|
this.mesh = null;
|
|
|
|
// Sound buffers and data
|
|
this.sounds = {};
|
|
if (data.sounds) {
|
|
for (var i = 0; i < data.sounds.length; i++) {
|
|
this.sounds[data.sounds[i]['soundName']] = data.sounds[i];
|
|
}
|
|
}
|
|
this.soundsLoaded = 0;
|
|
this.instrument = 1;
|
|
|
|
// Incremented when images are loaded by the browser.
|
|
this.costumesLoaded = 0;
|
|
|
|
// Stacks to be pushed to the interpreter and run
|
|
this.stacks = [];
|
|
};
|
|
|
|
// Attaches a Sprite (<img>) to a Scratch scene
|
|
Sprite.prototype.attach = function(scene) {
|
|
// Create textures and materials for each of the costumes.
|
|
for (var c in this.costumes) {
|
|
this.textures[c] = document.createElement('img');
|
|
$(this.textures[c])
|
|
.load([this, c], function(evo) {
|
|
var sprite = evo.handleObj.data[0];
|
|
var c = evo.handleObj.data[1];
|
|
|
|
sprite.costumesLoaded += 1;
|
|
sprite.updateCostume();
|
|
|
|
$(sprite.textures[c]).css('display', sprite.currentCostumeIndex == c ? 'inline' : 'none');
|
|
$(sprite.textures[c]).css('position', 'absolute').css('left', '0px').css('top', '0px');
|
|
$(sprite.textures[c]).bind('dragstart', function(evt) { evt.preventDefault(); })
|
|
.bind('selectstart', function(evt) { evt.preventDefault(); })
|
|
.bind('touchend', function(evt) { sprite.onClick(evt); $(this).addClass('touched'); })
|
|
.click(function(evt) {
|
|
if (!$(this).hasClass('touched')) {
|
|
sprite.onClick(evt);
|
|
} else {
|
|
$(this).removeClass('touched');
|
|
}
|
|
});
|
|
scene.append($(sprite.textures[c]));
|
|
})
|
|
.attr('src', io.asset_base + this.costumes[c].baseLayerMD5 + io.asset_suffix);
|
|
}
|
|
|
|
this.mesh = this.textures[this.currentCostumeIndex];
|
|
this.updateLayer();
|
|
this.updateVisible();
|
|
this.updateTransform();
|
|
|
|
this.talkBubble = $('<div class="bubble-container"></div>');
|
|
this.talkBubble.css('display', 'none');
|
|
this.talkBubbleBox = $('<div class="bubble"></div>');
|
|
this.talkBubbleStyler = $('<div class="bubble-say"></div>');
|
|
this.talkBubble.append(this.talkBubbleBox);
|
|
this.talkBubble.append(this.talkBubbleStyler);
|
|
|
|
runtime.scene.append(this.talkBubble);
|
|
};
|
|
|
|
// Load sounds from the server and buffer them
|
|
Sprite.prototype.loadSounds = function() {
|
|
var spr = this;
|
|
$.each(this.sounds, function(index, sound) {
|
|
io.soundRequest(sound, spr);
|
|
});
|
|
};
|
|
|
|
// True when all the costumes have been loaded
|
|
Sprite.prototype.isLoaded = function() {
|
|
return this.costumesLoaded == this.costumes.length && this.soundsLoaded == Object.keys(this.sounds).length;
|
|
};
|
|
|
|
// Step methods
|
|
Sprite.prototype.showCostume = function(costume) {
|
|
if (costume < 0) {
|
|
costume += this.costumes.length;
|
|
}
|
|
if (!this.textures[costume]) {
|
|
this.currentCostumeIndex = 0;
|
|
}
|
|
else {
|
|
this.currentCostumeIndex = costume;
|
|
}
|
|
this.updateCostume();
|
|
};
|
|
|
|
Sprite.prototype.indexOfCostumeNamed = function(name) {
|
|
for (var i in this.costumes) {
|
|
var c = this.costumes[i];
|
|
if (c['costumeName'] == name) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
Sprite.prototype.showCostumeNamed = function(name) {
|
|
var index = this.indexOfCostumeNamed(name);
|
|
if (!index) return;
|
|
this.showCostume(index);
|
|
};
|
|
|
|
Sprite.prototype.updateCostume = function() {
|
|
if (!this.textures[this.currentCostumeIndex]) {
|
|
this.currentCostumeIndex = 0;
|
|
}
|
|
$(this.mesh).css('display', 'none');
|
|
this.mesh = this.textures[this.currentCostumeIndex];
|
|
this.updateVisible();
|
|
this.updateTransform();
|
|
};
|
|
|
|
Sprite.prototype.onClick = function(evt) {
|
|
// TODO - needs work!!
|
|
var boxOffset = $('#container').offset();
|
|
var mouseX = runtime.mousePos[0] + 240 + boxOffset.left;
|
|
var mouseY = 180 - runtime.mousePos[1] + boxOffset.top;
|
|
|
|
if (this.mesh.src.indexOf('.svg') == -1) {
|
|
// HACK - if the image SRC doesn't indicate it's an SVG,
|
|
// then we'll try to detect if the point we clicked is transparent
|
|
// by rendering the sprite on a canvas. With an SVG,
|
|
// we are forced not to do this for now by Chrome/Webkit SOP:
|
|
// http://code.google.com/p/chromium/issues/detail?id=68568
|
|
var canv = document.createElement('canvas');
|
|
$(canv).css('width', 480).css('height', 360);
|
|
var ctx = canv.getContext('2d');
|
|
var drawWidth = this.textures[this.currentCostumeIndex].width * this.scale;
|
|
var drawHeight = this.textures[this.currentCostumeIndex].height * this.scale;
|
|
var rotationCenterX = this.costumes[this.currentCostumeIndex].rotationCenterX;
|
|
var rotationCenterY = this.costumes[this.currentCostumeIndex].rotationCenterY;
|
|
var drawX = this.scratchX + (480 / 2) - rotationCenterX;
|
|
var drawY = -this.scratchY + (360 / 2) - rotationCenterY;
|
|
ctx.rotate(this.rotation * Math.PI / 180.0);
|
|
ctx.drawImage(this.mesh, 0, 0, drawWidth, drawHeight);
|
|
|
|
var offsetX = mouseX - drawX - 9; // the 9 is a hack - webkit/moz
|
|
var offsetY = mouseY - drawY - 9;
|
|
|
|
var idata = ctx.getImageData(offsetX, offsetY, 1, 1).data;
|
|
var alpha = idata[3];
|
|
} else {
|
|
var alpha = 1;
|
|
}
|
|
|
|
if (alpha > 0) {
|
|
// Start clicked hats if the pixel is non-transparent
|
|
runtime.startClickedHats(this);
|
|
} else {
|
|
// Otherwise, move back a layer and trigger the click event
|
|
$(this.mesh).hide();
|
|
var underElement = document.elementFromPoint(mouseX, mouseY);
|
|
$(underElement).click();
|
|
$(this.mesh).show();
|
|
}
|
|
};
|
|
|
|
Sprite.prototype.setVisible = function(v) {
|
|
this.visible = v;
|
|
this.updateVisible();
|
|
};
|
|
|
|
Sprite.prototype.updateLayer = function() {
|
|
$(this.mesh).css('z-index', this.z);
|
|
if (this.talkBubble) this.talkBubble.css('z-index', this.z);
|
|
};
|
|
|
|
Sprite.prototype.updateVisible = function() {
|
|
$(this.mesh).css('display', this.visible ? 'inline' : 'none');
|
|
if (this.talkBubbleOn) {
|
|
this.talkBubble.css('display', this.visible ? 'inline-block' : 'none');
|
|
}
|
|
};
|
|
|
|
Sprite.prototype.updateTransform = function() {
|
|
var texture = this.textures[this.currentCostumeIndex];
|
|
var resolution = this.costumes[this.currentCostumeIndex].bitmapResolution || 1;
|
|
|
|
var drawWidth = texture.width * this.scale / resolution;
|
|
var drawHeight = texture.height * this.scale / resolution;
|
|
|
|
var rotationCenterX = this.costumes[this.currentCostumeIndex].rotationCenterX;
|
|
var rotationCenterY = this.costumes[this.currentCostumeIndex].rotationCenterY;
|
|
|
|
var drawX = this.scratchX + (480 / 2) - rotationCenterX;
|
|
var drawY = -this.scratchY + (360 / 2) - rotationCenterY;
|
|
|
|
var scaleXprepend = '';
|
|
if (this.isFlipped) {
|
|
scaleXprepend = '-'; // For a leftRight flip, we add a minus
|
|
// sign to the X scale.
|
|
}
|
|
|
|
$(this.mesh).css('transform',
|
|
'translatex(' + drawX + 'px) \
|
|
translatey(' + drawY + 'px) \
|
|
rotate(' + this.rotation + 'deg) \
|
|
scaleX(' + scaleXprepend + (this.scale / resolution) + ') scaleY(' + (this.scale / resolution) + ')');
|
|
$(this.mesh).css('-moz-transform',
|
|
'translatex(' + drawX + 'px) \
|
|
translatey(' + drawY + 'px) \
|
|
rotate(' + this.rotation + 'deg) \
|
|
scaleX(' + scaleXprepend + this.scale + ') scaleY(' + this.scale / resolution + ')');
|
|
$(this.mesh).css('-webkit-transform',
|
|
'translatex(' + drawX + 'px) \
|
|
translatey(' + drawY + 'px) \
|
|
rotate(' + this.rotation + 'deg) \
|
|
scaleX(' + scaleXprepend + (this.scale / resolution) + ') scaleY(' + (this.scale / resolution) + ')');
|
|
|
|
$(this.mesh).css('-webkit-transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
|
|
$(this.mesh).css('-moz-transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
|
|
$(this.mesh).css('-ms-transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
|
|
$(this.mesh).css('-o-transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
|
|
$(this.mesh).css('transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
|
|
|
|
// Don't forget to update the talk bubble.
|
|
if (this.talkBubble) {
|
|
var xy = this.getTalkBubbleXY();
|
|
this.talkBubble.css('left', xy[0] + 'px');
|
|
this.talkBubble.css('top', xy[1] + 'px');
|
|
}
|
|
|
|
this.updateLayer();
|
|
};
|
|
|
|
Sprite.prototype.getTalkBubbleXY = function() {
|
|
var texture = this.textures[this.currentCostumeIndex];
|
|
var drawWidth = texture.width * this.scale;
|
|
var drawHeight = texture.height * this.scale;
|
|
var rotationCenterX = this.costumes[this.currentCostumeIndex].rotationCenterX;
|
|
var rotationCenterY = this.costumes[this.currentCostumeIndex].rotationCenterY;
|
|
var drawX = this.scratchX + (480 / 2) - rotationCenterX;
|
|
var drawY = -this.scratchY + (360 / 2) - rotationCenterY;
|
|
return [drawX + drawWidth, drawY - drawHeight / 2];
|
|
};
|
|
|
|
Sprite.prototype.showBubble = function(text, type) {
|
|
var xy = this.getTalkBubbleXY();
|
|
|
|
this.talkBubbleOn = true;
|
|
this.talkBubble.css('z-index', this.z);
|
|
this.talkBubble.css('left', xy[0] + 'px');
|
|
this.talkBubble.css('top', xy[1] + 'px');
|
|
|
|
|
|
this.talkBubbleStyler.removeClass('bubble-say');
|
|
this.talkBubbleStyler.removeClass('bubble-think');
|
|
if (type == 'say') {
|
|
this.talkBubbleStyler.addClass('bubble-say');
|
|
} else if (type == 'think') {
|
|
this.talkBubbleStyler.addClass('bubble-think');
|
|
}
|
|
|
|
if (this.visible) {
|
|
this.talkBubble.css('display', 'inline-block');
|
|
}
|
|
this.talkBubbleBox.html(text);
|
|
};
|
|
|
|
Sprite.prototype.hideBubble = function() {
|
|
this.talkBubbleOn = false;
|
|
this.talkBubble.css('display', 'none');
|
|
};
|
|
|
|
Sprite.prototype.setXY = function(x, y) {
|
|
this.scratchX = x;
|
|
this.scratchY = y;
|
|
this.updateTransform();
|
|
};
|
|
|
|
Sprite.prototype.setDirection = function(d) {
|
|
var rotation;
|
|
d = d % 360
|
|
if (d < 0) d += 360;
|
|
this.direction = d > 180 ? d - 360 : d;
|
|
if (this.rotationStyle == 'normal') {
|
|
rotation = (this.direction - 90) % 360;
|
|
} else if (this.rotationStyle == 'leftRight') {
|
|
if (((this.direction - 90) % 360) >= 0) {
|
|
this.isFlipped = false;
|
|
} else {
|
|
this.isFlipped = true;
|
|
}
|
|
rotation = 0;
|
|
} else {
|
|
rotation = 0;
|
|
}
|
|
this.rotation = rotation;
|
|
this.updateTransform();
|
|
};
|
|
|
|
Sprite.prototype.setRotationStyle = function(r) {
|
|
this.rotationStyle = r;
|
|
};
|
|
|
|
Sprite.prototype.getSize = function() {
|
|
return this.scale * 100;
|
|
};
|
|
|
|
Sprite.prototype.setSize = function(percent) {
|
|
var newScale = percent / 100.0;
|
|
newScale = Math.max(0.05, Math.min(newScale, 100));
|
|
this.scale = newScale;
|
|
this.updateTransform();
|
|
};
|
|
|
|
// Move functions
|
|
Sprite.prototype.keepOnStage = function() {
|
|
var x = this.scratchX + 240;
|
|
var y = 180 - this.scratchY;
|
|
var myBox = this.getRect();
|
|
var inset = -Math.min(18, Math.min(myBox.width, myBox.height) / 2);
|
|
var edgeBox = new Rectangle(inset, inset, 480 - (2 * inset), 360 - (2 * inset));
|
|
if (myBox.intersects(edgeBox)) return; // sprite is sufficiently on stage
|
|
if (myBox.right < edgeBox.left) x += edgeBox.left - myBox.right;
|
|
if (myBox.left > edgeBox.right) x -= myBox.left - edgeBox.right;
|
|
if (myBox.bottom < edgeBox.top) y += edgeBox.top - myBox.bottom;
|
|
if (myBox.top > edgeBox.bottom) y -= myBox.top - edgeBox.bottom;
|
|
this.scratchX = x - 240;
|
|
this.scratchY = 180 - y;
|
|
};
|
|
|
|
Sprite.prototype.getRect = function() {
|
|
var cImg = this.textures[this.currentCostumeIndex];
|
|
var x = this.scratchX + 240 - (cImg.width/2.0);
|
|
var y = 180 - this.scratchY - (cImg.height/2.0);
|
|
var myBox = new Rectangle(x, y, cImg.width, cImg.height);
|
|
return myBox;
|
|
};
|
|
|
|
// Pen functions
|
|
Sprite.prototype.setPenColor = function(c) {
|
|
var hsv = Color.rgb2hsv(c);
|
|
this.penHue = (200 * hsv[0]) / 360 ;
|
|
this.penShade = 50 * hsv[2]; // not quite right; doesn't account for saturation
|
|
this.penColorCache = c;
|
|
};
|
|
|
|
Sprite.prototype.setPenHue = function(n) {
|
|
this.penHue = n % 200;
|
|
if (this.penHue < 0) this.penHue += 200;
|
|
this.updateCachedPenColor();
|
|
};
|
|
|
|
Sprite.prototype.setPenShade = function(n) {
|
|
this.penShade = n % 200;
|
|
if (this.penShade < 0) this.penShade += 200;
|
|
this.updateCachedPenColor();
|
|
};
|
|
|
|
Sprite.prototype.updateCachedPenColor = function() {
|
|
var c = Color.fromHSV((this.penHue * 180.0) / 100.0, 1, 1);
|
|
var shade = this.penShade > 100 ? 200 - this.penShade : this.penShade; // range 0..100
|
|
if (shade < 50) {
|
|
this.penColorCache = Color.mixRGB(0, c, (10 + shade) / 60.0);
|
|
} else {
|
|
this.penColorCache = Color.mixRGB(c, 0xFFFFFF, (shade - 50) / 60);
|
|
}
|
|
};
|
|
|
|
Sprite.prototype.stamp = function(canvas, opacity) {
|
|
var drawWidth = this.textures[this.currentCostumeIndex].width * this.scale;
|
|
var drawHeight = this.textures[this.currentCostumeIndex].height * this.scale;
|
|
var drawX = this.scratchX + (480 / 2);
|
|
var drawY = -this.scratchY + (360 / 2);
|
|
canvas.globalAlpha = opacity / 100.0;
|
|
canvas.save();
|
|
canvas.translate(drawX, drawY);
|
|
canvas.rotate(this.rotation * Math.PI / 180.0);
|
|
canvas.drawImage(this.mesh, -drawWidth/2, -drawHeight/2, drawWidth, drawHeight);
|
|
canvas.restore();
|
|
canvas.globalAlpha = 1;
|
|
};
|
|
|
|
Sprite.prototype.soundNamed = function(name) {
|
|
if (name in this.sounds && this.sounds[name].buffer) {
|
|
return this.sounds[name];
|
|
} else if (name in runtime.stage.sounds && runtime.stage.sounds[name].buffer) {
|
|
return runtime.stage.sounds[name];
|
|
}
|
|
return null;
|
|
};
|