Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "touching edge" and tight bounding box functionality #119

Merged
merged 8 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 102 additions & 102 deletions src/Renderer.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import Matrix from "./renderer/Matrix.js";
import Drawable from "./renderer/Drawable.js";
import BitmapSkin from "./renderer/BitmapSkin.js";
import PenSkin from "./renderer/PenSkin.js";
import SpeechBubbleSkin from "./renderer/SpeechBubbleSkin.js";
import VectorSkin from "./renderer/VectorSkin.js";
import Rectangle from "./renderer/Rectangle.js";
import ShaderManager from "./renderer/ShaderManager.js";
import SkinCache from "./renderer/SkinCache.js";
import { effectBitmasks } from "./renderer/effectInfo.js";
import { effectNames, effectBitmasks } from "./renderer/effectInfo.js";

import { Sprite, Stage } from "./Sprite.js";
import Costume from "./Costume.js";

// Rectangle used for checking collision bounds.
// Rather than create a new one each time, we can just reuse this one.
const __collisionBox = new Rectangle();

export default class Renderer {
constructor(project, renderTarget) {
Expand All @@ -22,7 +29,8 @@ export default class Renderer {
}

this._shaderManager = new ShaderManager(this);
this._skinCache = new SkinCache(this);
this._drawables = new WeakMap();
this._skins = new WeakMap();

this._currentShader = null;
this._currentFramebuffer = null;
Expand Down Expand Up @@ -63,6 +71,38 @@ export default class Renderer {
);
}

// Retrieve a given object (e.g. costume or speech bubble)'s skin. If it doesn't exist, make one.
_getSkin(obj) {
if (this._skins.has(obj)) {
return this._skins.get(obj);
}

let skin;

if (obj instanceof Costume) {
if (obj.isBitmap) {
skin = new BitmapSkin(this, obj.img);
} else {
skin = new VectorSkin(this, obj.img);
}
} else {
// If it's not a costume, assume it's a speech bubble.
skin = new SpeechBubbleSkin(this, obj);
}
this._skins.set(obj, skin);
return skin;
}

// Retrieve the renderer-specific data object for a given sprite or clone. If it doesn't exist, make one.
_getDrawable(sprite) {
if (this._drawables.has(sprite)) {
return this._drawables.get(sprite);
}
const drawable = new Drawable(this, sprite);
this._drawables.set(sprite, drawable);
return drawable;
}

// Create a framebuffer info object, which contains the following:
// * The framebuffer itself.
// * The texture backing the framebuffer.
Expand Down Expand Up @@ -219,14 +259,12 @@ export default class Renderer {
);
Matrix.translate(penMatrix, penMatrix, -0.5, -0.5);

this._setSkinUniforms(
this._renderSkin(
this._penSkin,
options.drawMode,
penMatrix,
1,
null
1 /* scale */
);
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
}

// Sprites + clones
Expand Down Expand Up @@ -291,10 +329,7 @@ export default class Renderer {
gl.clearColor(1, 1, 1, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

// TODO: find a way to not destroy the skins of hidden sprites
towerofnix marked this conversation as resolved.
Show resolved Hide resolved
this._skinCache.beginTrace();
this._renderLayers();
this._skinCache.endTrace();
}

createStage(w, h) {
Expand All @@ -316,21 +351,45 @@ export default class Renderer {
return stage;
}

_setSkinUniforms(skin, drawMode, matrix, scale, effects, effectMask) {
// Calculate the transform matrix for a speech bubble attached to a sprite.
_calculateSpeechBubbleMatrix(spr, speechBubbleSkin) {
const sprBounds = this.getBoundingBox(spr);
let x;
if (
speechBubbleSkin.width + sprBounds.right >
this.project.stage.width / 2
) {
x = sprBounds.left - speechBubbleSkin.width;
speechBubbleSkin.flipped = true;
} else {
x = sprBounds.right;
speechBubbleSkin.flipped = false;
}
x = Math.round(x - speechBubbleSkin.offsetX);
const y = Math.round(sprBounds.top - speechBubbleSkin.offsetY);

const m = Matrix.create();
Matrix.translate(m, m, x, y);
Matrix.scale(m, m, speechBubbleSkin.width, speechBubbleSkin.height);

return m;
}

_renderSkin(skin, drawMode, matrix, scale, effects, effectMask, colorMask) {
const gl = this.gl;

const skinTexture = skin.getTexture(scale * this._screenSpaceScale);
// Skip rendering the skin if it has no texture.
if (!skinTexture) return;

let effectBitmask = 0;
if (effects) effectBitmask = effects._bitmask;
let effectBitmask = effects ? effects._bitmask : 0;
if (typeof effectMask === "number") effectBitmask &= effectMask;
const shader = this._shaderManager.getShader(drawMode, effectBitmask);
this._setShader(shader);
gl.uniformMatrix3fv(shader.uniforms.u_transform, false, matrix);

if (effectBitmask !== 0) {
for (const effect of Object.keys(effects._effectValues)) {
for (const effect of effectNames) {
const effectVal = effects._effectValues[effect];
if (effectVal !== 0)
gl.uniform1f(shader.uniforms[`u_${effect}`], effectVal);
Expand All @@ -344,114 +403,52 @@ export default class Renderer {
gl.bindTexture(gl.TEXTURE_2D, skinTexture);
// All textures are bound to texture unit 0, so that's where the texture sampler should point
gl.uniform1i(shader.uniforms.u_texture, 0);
}

// Calculate the transform matrix for a sprite.
// TODO: store the transform matrix in the sprite itself. That adds some complexity though,
// so it's better off in another PR.
_calculateSpriteMatrix(spr) {
// These transforms are actually in reverse order because lol matrices
const m = Matrix.create();
if (!(spr instanceof Stage)) {
Matrix.translate(m, m, spr.x, spr.y);
switch (spr.rotationStyle) {
case Sprite.RotationStyle.ALL_AROUND: {
Matrix.rotate(m, m, spr.scratchToRad(spr.direction));
break;
}
case Sprite.RotationStyle.LEFT_RIGHT: {
if (spr.direction < 0) Matrix.scale(m, m, -1, 1);
break;
}
}

const spriteScale = spr.size / 100;
Matrix.scale(m, m, spriteScale, spriteScale);
}

const scalingFactor = 1 / spr.costume.resolution;
// Rotation centers are in non-Scratch space (positive y-values = down),
// but these transforms are in Scratch space (negative y-values = down).
Matrix.translate(
m,
m,
-spr.costume.center.x * scalingFactor,
(spr.costume.center.y - spr.costume.height) * scalingFactor
);
Matrix.scale(
m,
m,
spr.costume.width * scalingFactor,
spr.costume.height * scalingFactor
);

return m;
}

// Calculate the transform matrix for a speech bubble attached to a sprite.
_calculateSpeechBubbleMatrix(spr, speechBubbleSkin) {
const sprBounds = this.getBoundingBox(spr);
let x;
if (
speechBubbleSkin.width + sprBounds.right >
this.project.stage.width / 2
) {
x = sprBounds.left - speechBubbleSkin.width;
speechBubbleSkin.flipped = true;
} else {
x = sprBounds.right;
speechBubbleSkin.flipped = false;
}
x = Math.round(x - speechBubbleSkin.offsetX);
const y = Math.round(sprBounds.top - speechBubbleSkin.offsetY);
// Enable color masking mode if set
if (Array.isArray(colorMask))
this.gl.uniform4fv(this._currentShader.uniforms.u_colorMask, colorMask);

const m = Matrix.create();
Matrix.translate(m, m, x, y);
Matrix.scale(m, m, speechBubbleSkin.width, speechBubbleSkin.height);

return m;
// Actually draw the skin
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
}

renderSprite(sprite, options) {
const spriteScale = Object.prototype.hasOwnProperty.call(sprite, "size")
? sprite.size / 100
: 1;

this._setSkinUniforms(
this._skinCache.getSkin(sprite.costume),
this._renderSkin(
this._getSkin(sprite.costume),
options.drawMode,
this._calculateSpriteMatrix(sprite),
this._getDrawable(sprite).getMatrix(),
spriteScale,
sprite.effects,
options.effectMask
options.effectMask,
options.colorMask
);
if (Array.isArray(options.colorMask))
this.gl.uniform4fv(
this._currentShader.uniforms.u_colorMask,
options.colorMask
);
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);

if (
options.renderSpeechBubbles &&
sprite._speechBubble &&
sprite._speechBubble.text !== ""
) {
const speechBubbleSkin = this._skinCache.getSkin(sprite._speechBubble);
const speechBubbleSkin = this._getSkin(sprite._speechBubble);

this._setSkinUniforms(
this._renderSkin(
speechBubbleSkin,
options.drawMode,
this._calculateSpeechBubbleMatrix(sprite, speechBubbleSkin),
1,
null
1 /* spriteScale */
);
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
}
}

getTightBoundingBox(sprite) {
return this._getDrawable(sprite).getTightBoundingBox();
}

getBoundingBox(sprite) {
return Rectangle.fromMatrix(this._calculateSpriteMatrix(sprite));
return Rectangle.fromMatrix(this._getDrawable(sprite).getMatrix());
}

// Mask drawing in to only areas where this sprite is opaque.
Expand Down Expand Up @@ -510,7 +507,10 @@ export default class Renderer {
}
}

const sprBox = this.getBoundingBox(spr).snapToInt();
const sprBox = Rectangle.copy(
this.getBoundingBox(spr),
__collisionBox
).snapToInt();

// This is an "impossible rectangle"-- its left bound is infinitely far to the right,
// its right bound is infinitely to the left, and so on. Its size is effectively -Infinity.
Expand All @@ -522,12 +522,9 @@ export default class Renderer {
-Infinity
);
for (const target of targets) {
Rectangle.union(
targetBox,
this.getBoundingBox(target).snapToInt(),
targetBox
);
Rectangle.union(targetBox, this.getBoundingBox(target), targetBox);
}
targetBox.snapToInt();

if (!sprBox.intersects(targetBox)) return false;
if (fast) return true;
Expand Down Expand Up @@ -580,7 +577,10 @@ export default class Renderer {
}

checkColorCollision(spr, targetsColor, sprColor) {
const sprBox = this.getBoundingBox(spr).snapToInt();
const sprBox = Rectangle.copy(
this.getBoundingBox(spr),
__collisionBox
).snapToInt();

const cx = this._collisionBuffer.width / 2;
const cy = this._collisionBuffer.height / 2;
Expand Down
11 changes: 11 additions & 0 deletions src/Sprite.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,17 @@ export class Sprite extends SpriteBase {
},
fast
);
case "edge": {
const bounds = this._project.renderer.getTightBoundingBox(this);
const stageWidth = this.stage.width;
const stageHeight = this.stage.height;
return (
bounds.left < -stageWidth / 2 ||
bounds.right > stageWidth / 2 ||
bounds.top > stageHeight / 2 ||
bounds.bottom < -stageHeight / 2
);
}
default:
console.error(
`Cannot find target "${target}" in "touching". Did you mean to pass a sprite class instead?`
Expand Down
18 changes: 18 additions & 0 deletions src/renderer/BitmapSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,29 @@ export default class BitmapSkin extends Skin {
super(renderer);

this._image = image;
this._imageData = null;
this._texture = null;

this._setSizeFromImage(image);
}

getImageData() {
// Make sure to handle potentially non-loaded textures
if (!this._image.complete) return null;

if (!this._imageData) {
const canvas = document.createElement("canvas");
canvas.width = this._image.naturalWidth || this._image.width;
canvas.height = this._image.naturalHeight || this._image.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(this._image, 0, 0);
// Cache image data so we can reuse it
this._imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
}

return this._imageData;
}

getTexture() {
// Make sure to handle potentially non-loaded textures
const image = this._image;
Expand Down
Loading