mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-11 09:11:50 +00:00
make tiled sprites pixel-aligned to avoid transparent seams
This commit is contained in:
parent
838f26f198
commit
731510007b
@ -83,8 +83,20 @@ let warningsShown = 0;
|
||||
* @param {number} param0.h
|
||||
* @param {number} param0.originalW
|
||||
* @param {number} param0.originalH
|
||||
* @param {boolean=} param0.pixelAligned
|
||||
* Whether to round the canvas coordinates, to avoid issues with transparency between tiling images
|
||||
*/
|
||||
export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, originalH }) {
|
||||
export function drawSpriteClipped({
|
||||
parameters,
|
||||
sprite,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
originalW,
|
||||
originalH,
|
||||
pixelAligned = false,
|
||||
}) {
|
||||
const rect = new Rectangle(x, y, w, h);
|
||||
const intersection = rect.getIntersection(parameters.visibleRect);
|
||||
if (!intersection) {
|
||||
@ -103,6 +115,38 @@ export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, o
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pixelAligned) {
|
||||
parameters.context.drawImage(
|
||||
sprite,
|
||||
|
||||
// src pos and size
|
||||
((intersection.x - x) / w) * originalW,
|
||||
((intersection.y - y) / h) * originalH,
|
||||
(originalW * intersection.w) / w,
|
||||
(originalH * intersection.h) / h,
|
||||
|
||||
// dest pos and size
|
||||
intersection.x,
|
||||
intersection.y,
|
||||
intersection.w,
|
||||
intersection.h
|
||||
);
|
||||
}
|
||||
|
||||
const matrix = parameters.context.getTransform();
|
||||
let { x: x1, y: y1 } = matrix.transformPoint(new DOMPoint(intersection.x, intersection.y));
|
||||
let { x: x2, y: y2 } = matrix.transformPoint(
|
||||
new DOMPoint(intersection.x + intersection.w, intersection.y + intersection.h)
|
||||
);
|
||||
x1 = Math.round(x1);
|
||||
y1 = Math.round(y1);
|
||||
x2 = Math.round(x2);
|
||||
y2 = Math.round(y2);
|
||||
if (x2 - x1 == 0 || y2 - y1 == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
parameters.context.resetTransform();
|
||||
parameters.context.drawImage(
|
||||
sprite,
|
||||
|
||||
@ -113,9 +157,10 @@ export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, o
|
||||
(originalH * intersection.h) / h,
|
||||
|
||||
// dest pos and size
|
||||
intersection.x,
|
||||
intersection.y,
|
||||
intersection.w,
|
||||
intersection.h
|
||||
x1,
|
||||
y1,
|
||||
x2 - x1,
|
||||
y2 - y1
|
||||
);
|
||||
parameters.context.setTransform(matrix);
|
||||
}
|
||||
|
||||
@ -5,8 +5,6 @@ import { round3Digits } from "./utils";
|
||||
export const ORIGINAL_SPRITE_SCALE = "0.75";
|
||||
export const FULL_CLIP_RECT = new Rectangle(0, 0, 1, 1);
|
||||
|
||||
const EXTRUDE = 0.1;
|
||||
|
||||
export class BaseSprite {
|
||||
/**
|
||||
* Returns the raw handle
|
||||
@ -227,10 +225,10 @@ export class AtlasSprite extends BaseSprite {
|
||||
srcH,
|
||||
|
||||
// dest pos and size
|
||||
destX - EXTRUDE,
|
||||
destY - EXTRUDE,
|
||||
destW + 2 * EXTRUDE,
|
||||
destH + 2 * EXTRUDE
|
||||
destX,
|
||||
destY,
|
||||
destW,
|
||||
destH
|
||||
);
|
||||
}
|
||||
|
||||
@ -291,10 +289,10 @@ export class AtlasSprite extends BaseSprite {
|
||||
srcH,
|
||||
|
||||
// dest pos and size
|
||||
destX - EXTRUDE,
|
||||
destY - EXTRUDE,
|
||||
destW + 2 * EXTRUDE,
|
||||
destH + 2 * EXTRUDE
|
||||
destX,
|
||||
destY,
|
||||
destW,
|
||||
destH
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -255,8 +255,16 @@ export class StaticMapEntityComponent extends Component {
|
||||
* @param {AtlasSprite} sprite
|
||||
* @param {number=} extrudePixels How many pixels to extrude the sprite
|
||||
* @param {Vector=} overridePosition Whether to drwa the entity at a different location
|
||||
* @param {boolean=} pixelAligned
|
||||
* Whether to round the canvas coordinates, to avoid issues with transparency between tiling images
|
||||
*/
|
||||
drawSpriteOnBoundsClipped(parameters, sprite, extrudePixels = 0, overridePosition = null) {
|
||||
drawSpriteOnBoundsClipped(
|
||||
parameters,
|
||||
sprite,
|
||||
extrudePixels = 0,
|
||||
overridePosition = null,
|
||||
pixelAligned = false
|
||||
) {
|
||||
if (!this.shouldBeDrawn(parameters) && !overridePosition) {
|
||||
return;
|
||||
}
|
||||
@ -269,31 +277,68 @@ export class StaticMapEntityComponent extends Component {
|
||||
worldY = overridePosition.y * globalConfig.tileSize;
|
||||
}
|
||||
|
||||
if (this.rotation === 0) {
|
||||
// Early out, is faster
|
||||
sprite.drawCached(
|
||||
parameters,
|
||||
worldX - extrudePixels * size.x,
|
||||
worldY - extrudePixels * size.y,
|
||||
globalConfig.tileSize * size.x + 2 * extrudePixels * size.x,
|
||||
globalConfig.tileSize * size.y + 2 * extrudePixels * size.y
|
||||
);
|
||||
} else {
|
||||
const rotationCenterX = worldX + globalConfig.halfTileSize;
|
||||
const rotationCenterY = worldY + globalConfig.halfTileSize;
|
||||
if (!pixelAligned) {
|
||||
if (this.rotation === 0) {
|
||||
// Early out, is faster
|
||||
sprite.drawCached(
|
||||
parameters,
|
||||
worldX - extrudePixels * size.x,
|
||||
worldY - extrudePixels * size.y,
|
||||
globalConfig.tileSize * size.x + 2 * extrudePixels * size.x,
|
||||
globalConfig.tileSize * size.y + 2 * extrudePixels * size.y
|
||||
);
|
||||
} else {
|
||||
const rotationCenterX = worldX + globalConfig.halfTileSize;
|
||||
const rotationCenterY = worldY + globalConfig.halfTileSize;
|
||||
|
||||
parameters.context.translate(rotationCenterX, rotationCenterY);
|
||||
parameters.context.rotate(Math.radians(this.rotation));
|
||||
sprite.drawCached(
|
||||
parameters,
|
||||
-globalConfig.halfTileSize - extrudePixels * size.x,
|
||||
-globalConfig.halfTileSize - extrudePixels * size.y,
|
||||
globalConfig.tileSize * size.x + 2 * extrudePixels * size.x,
|
||||
globalConfig.tileSize * size.y + 2 * extrudePixels * size.y,
|
||||
false // no clipping possible here
|
||||
);
|
||||
parameters.context.rotate(-Math.radians(this.rotation));
|
||||
parameters.context.translate(-rotationCenterX, -rotationCenterY);
|
||||
parameters.context.translate(rotationCenterX, rotationCenterY);
|
||||
parameters.context.rotate(Math.radians(this.rotation));
|
||||
sprite.drawCached(
|
||||
parameters,
|
||||
-globalConfig.halfTileSize - extrudePixels * size.x,
|
||||
-globalConfig.halfTileSize - extrudePixels * size.y,
|
||||
globalConfig.tileSize * size.x + 2 * extrudePixels * size.x,
|
||||
globalConfig.tileSize * size.y + 2 * extrudePixels * size.y,
|
||||
false // no clipping possible here
|
||||
);
|
||||
parameters.context.rotate(-Math.radians(this.rotation));
|
||||
parameters.context.translate(-rotationCenterX, -rotationCenterY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const transform = parameters.context.getTransform();
|
||||
const matrix = new DOMMatrix().rotate(0, 0, -this.rotation).multiplySelf(transform);
|
||||
let { x: x1, y: y1 } = matrix.transformPoint(
|
||||
new DOMPoint(worldX - extrudePixels * size.x, worldY - extrudePixels * size.y)
|
||||
);
|
||||
let { x: x2, y: y2 } = matrix.transformPoint(
|
||||
new DOMPoint(
|
||||
worldX + globalConfig.tileSize * size.x + extrudePixels * size.x,
|
||||
worldY + globalConfig.tileSize * size.y + extrudePixels * size.y
|
||||
)
|
||||
);
|
||||
if (x1 > x2) {
|
||||
[x1, x2] = [x2, x1];
|
||||
}
|
||||
if (y1 > y2) {
|
||||
[y1, y2] = [y2, y1];
|
||||
}
|
||||
// Even though drawCached may scale the coordinates,
|
||||
// that scaling is for sprites that don't take up their full tile space,
|
||||
// so they should be interpolated exactly between the rounded tile coordinates.
|
||||
// E.g. rounding in drawCached causes curved belts to look misaligned.
|
||||
x1 = Math.round(x1);
|
||||
y1 = Math.round(y1);
|
||||
x2 = Math.round(x2);
|
||||
y2 = Math.round(y2);
|
||||
if (x2 - x1 == 0 || y2 - y1 == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
parameters.context.resetTransform();
|
||||
parameters.context.rotate(Math.radians(this.rotation));
|
||||
sprite.drawCached(parameters, x1, y1, x2 - x1, y2 - y1, false);
|
||||
parameters.context.setTransform(transform);
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,19 +106,19 @@ export class MapChunkAggregate {
|
||||
});
|
||||
|
||||
const dims = globalConfig.mapChunkWorldSize * globalConfig.chunkAggregateSize;
|
||||
const extrude = 0.05;
|
||||
|
||||
// Draw chunk "pixel" art
|
||||
parameters.context.imageSmoothingEnabled = false;
|
||||
drawSpriteClipped({
|
||||
parameters,
|
||||
sprite,
|
||||
x: this.x * dims - extrude,
|
||||
y: this.y * dims - extrude,
|
||||
w: dims + 2 * extrude,
|
||||
h: dims + 2 * extrude,
|
||||
x: this.x * dims,
|
||||
y: this.y * dims,
|
||||
w: dims,
|
||||
h: dims,
|
||||
originalW: aggregateOverlaySize,
|
||||
originalH: aggregateOverlaySize,
|
||||
pixelAligned: true,
|
||||
});
|
||||
|
||||
parameters.context.imageSmoothingEnabled = true;
|
||||
|
||||
@ -533,7 +533,13 @@ export class BeltSystem extends GameSystem {
|
||||
}
|
||||
|
||||
// Culling happens within the static map entity component
|
||||
entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0);
|
||||
entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(
|
||||
parameters,
|
||||
sprite,
|
||||
0,
|
||||
null,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -544,7 +550,13 @@ export class BeltSystem extends GameSystem {
|
||||
const sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT];
|
||||
|
||||
// Culling happens within the static map entity component
|
||||
entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0);
|
||||
entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(
|
||||
parameters,
|
||||
sprite,
|
||||
0,
|
||||
null,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,20 +278,36 @@ export class BeltUnderlaysSystem extends GameSystem {
|
||||
((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) *
|
||||
globalConfig.itemSpacingOnBelts
|
||||
);
|
||||
parameters.context.translate(x, y);
|
||||
|
||||
// See components/static_map_entity.js:drawSpriteOnBoundsClipped
|
||||
const transform = parameters.context.getTransform();
|
||||
const matrix = new DOMMatrix().rotate(0, 0, -angle).multiplySelf(transform);
|
||||
let { x: x1, y: y1 } = matrix.transformPoint(
|
||||
new DOMPoint(x - globalConfig.halfTileSize, y - globalConfig.halfTileSize)
|
||||
);
|
||||
let { x: x2, y: y2 } = matrix.transformPoint(
|
||||
new DOMPoint(x + globalConfig.halfTileSize, y + globalConfig.halfTileSize)
|
||||
);
|
||||
if (x1 > x2) {
|
||||
[x1, x2] = [x2, x1];
|
||||
}
|
||||
if (y1 > y2) {
|
||||
[y1, y2] = [y2, y1];
|
||||
}
|
||||
x1 = Math.round(x1);
|
||||
y1 = Math.round(y1);
|
||||
x2 = Math.round(x2);
|
||||
y2 = Math.round(y2);
|
||||
if (x2 - x1 == 0 || y2 - y1 == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
parameters.context.resetTransform();
|
||||
parameters.context.rotate(angleRadians);
|
||||
this.underlayBeltSprites[
|
||||
animationIndex % this.underlayBeltSprites.length
|
||||
].drawCachedWithClipRect(
|
||||
parameters,
|
||||
-globalConfig.halfTileSize,
|
||||
-globalConfig.halfTileSize,
|
||||
globalConfig.tileSize,
|
||||
globalConfig.tileSize,
|
||||
clipRect
|
||||
);
|
||||
parameters.context.rotate(-angleRadians);
|
||||
parameters.context.translate(-x, -y);
|
||||
].drawCachedWithClipRect(parameters, x1, y1, x2 - x1, y2 - y1, clipRect);
|
||||
parameters.context.setTransform(transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ export class MapResourcesSystem extends GameSystem {
|
||||
h: globalConfig.mapChunkWorldSize,
|
||||
originalW: globalConfig.mapChunkSize,
|
||||
originalH: globalConfig.mapChunkSize,
|
||||
pixelAligned: true,
|
||||
});
|
||||
parameters.context.imageSmoothingEnabled = true;
|
||||
|
||||
|
||||
@ -617,7 +617,7 @@ export class WireSystem extends GameSystem {
|
||||
assert(sprite, "Unknown wire type: " + wireType);
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
parameters.context.globalAlpha = opacity;
|
||||
staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 0);
|
||||
staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 0, null, true);
|
||||
|
||||
// DEBUG Rendering
|
||||
if (G_IS_DEV && globalConfig.debug.renderWireRotations) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user