mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-13 18:21:51 +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.h
|
||||||
* @param {number} param0.originalW
|
* @param {number} param0.originalW
|
||||||
* @param {number} param0.originalH
|
* @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 rect = new Rectangle(x, y, w, h);
|
||||||
const intersection = rect.getIntersection(parameters.visibleRect);
|
const intersection = rect.getIntersection(parameters.visibleRect);
|
||||||
if (!intersection) {
|
if (!intersection) {
|
||||||
@ -103,6 +115,38 @@ export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, o
|
|||||||
return;
|
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(
|
parameters.context.drawImage(
|
||||||
sprite,
|
sprite,
|
||||||
|
|
||||||
@ -113,9 +157,10 @@ export function drawSpriteClipped({ parameters, sprite, x, y, w, h, originalW, o
|
|||||||
(originalH * intersection.h) / h,
|
(originalH * intersection.h) / h,
|
||||||
|
|
||||||
// dest pos and size
|
// dest pos and size
|
||||||
intersection.x,
|
x1,
|
||||||
intersection.y,
|
y1,
|
||||||
intersection.w,
|
x2 - x1,
|
||||||
intersection.h
|
y2 - y1
|
||||||
);
|
);
|
||||||
|
parameters.context.setTransform(matrix);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import { round3Digits } from "./utils";
|
|||||||
export const ORIGINAL_SPRITE_SCALE = "0.75";
|
export const ORIGINAL_SPRITE_SCALE = "0.75";
|
||||||
export const FULL_CLIP_RECT = new Rectangle(0, 0, 1, 1);
|
export const FULL_CLIP_RECT = new Rectangle(0, 0, 1, 1);
|
||||||
|
|
||||||
const EXTRUDE = 0.1;
|
|
||||||
|
|
||||||
export class BaseSprite {
|
export class BaseSprite {
|
||||||
/**
|
/**
|
||||||
* Returns the raw handle
|
* Returns the raw handle
|
||||||
@ -227,10 +225,10 @@ export class AtlasSprite extends BaseSprite {
|
|||||||
srcH,
|
srcH,
|
||||||
|
|
||||||
// dest pos and size
|
// dest pos and size
|
||||||
destX - EXTRUDE,
|
destX,
|
||||||
destY - EXTRUDE,
|
destY,
|
||||||
destW + 2 * EXTRUDE,
|
destW,
|
||||||
destH + 2 * EXTRUDE
|
destH
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,10 +289,10 @@ export class AtlasSprite extends BaseSprite {
|
|||||||
srcH,
|
srcH,
|
||||||
|
|
||||||
// dest pos and size
|
// dest pos and size
|
||||||
destX - EXTRUDE,
|
destX,
|
||||||
destY - EXTRUDE,
|
destY,
|
||||||
destW + 2 * EXTRUDE,
|
destW,
|
||||||
destH + 2 * EXTRUDE
|
destH
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -255,8 +255,16 @@ export class StaticMapEntityComponent extends Component {
|
|||||||
* @param {AtlasSprite} sprite
|
* @param {AtlasSprite} sprite
|
||||||
* @param {number=} extrudePixels How many pixels to extrude the sprite
|
* @param {number=} extrudePixels How many pixels to extrude the sprite
|
||||||
* @param {Vector=} overridePosition Whether to drwa the entity at a different location
|
* @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) {
|
if (!this.shouldBeDrawn(parameters) && !overridePosition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -269,31 +277,68 @@ export class StaticMapEntityComponent extends Component {
|
|||||||
worldY = overridePosition.y * globalConfig.tileSize;
|
worldY = overridePosition.y * globalConfig.tileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.rotation === 0) {
|
if (!pixelAligned) {
|
||||||
// Early out, is faster
|
if (this.rotation === 0) {
|
||||||
sprite.drawCached(
|
// Early out, is faster
|
||||||
parameters,
|
sprite.drawCached(
|
||||||
worldX - extrudePixels * size.x,
|
parameters,
|
||||||
worldY - extrudePixels * size.y,
|
worldX - extrudePixels * size.x,
|
||||||
globalConfig.tileSize * size.x + 2 * extrudePixels * size.x,
|
worldY - extrudePixels * size.y,
|
||||||
globalConfig.tileSize * size.y + 2 * 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;
|
} else {
|
||||||
const rotationCenterY = worldY + globalConfig.halfTileSize;
|
const rotationCenterX = worldX + globalConfig.halfTileSize;
|
||||||
|
const rotationCenterY = worldY + globalConfig.halfTileSize;
|
||||||
|
|
||||||
parameters.context.translate(rotationCenterX, rotationCenterY);
|
parameters.context.translate(rotationCenterX, rotationCenterY);
|
||||||
parameters.context.rotate(Math.radians(this.rotation));
|
parameters.context.rotate(Math.radians(this.rotation));
|
||||||
sprite.drawCached(
|
sprite.drawCached(
|
||||||
parameters,
|
parameters,
|
||||||
-globalConfig.halfTileSize - extrudePixels * size.x,
|
-globalConfig.halfTileSize - extrudePixels * size.x,
|
||||||
-globalConfig.halfTileSize - extrudePixels * size.y,
|
-globalConfig.halfTileSize - extrudePixels * size.y,
|
||||||
globalConfig.tileSize * size.x + 2 * extrudePixels * size.x,
|
globalConfig.tileSize * size.x + 2 * extrudePixels * size.x,
|
||||||
globalConfig.tileSize * size.y + 2 * extrudePixels * size.y,
|
globalConfig.tileSize * size.y + 2 * extrudePixels * size.y,
|
||||||
false // no clipping possible here
|
false // no clipping possible here
|
||||||
);
|
);
|
||||||
parameters.context.rotate(-Math.radians(this.rotation));
|
parameters.context.rotate(-Math.radians(this.rotation));
|
||||||
parameters.context.translate(-rotationCenterX, -rotationCenterY);
|
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 dims = globalConfig.mapChunkWorldSize * globalConfig.chunkAggregateSize;
|
||||||
const extrude = 0.05;
|
|
||||||
|
|
||||||
// Draw chunk "pixel" art
|
// Draw chunk "pixel" art
|
||||||
parameters.context.imageSmoothingEnabled = false;
|
parameters.context.imageSmoothingEnabled = false;
|
||||||
drawSpriteClipped({
|
drawSpriteClipped({
|
||||||
parameters,
|
parameters,
|
||||||
sprite,
|
sprite,
|
||||||
x: this.x * dims - extrude,
|
x: this.x * dims,
|
||||||
y: this.y * dims - extrude,
|
y: this.y * dims,
|
||||||
w: dims + 2 * extrude,
|
w: dims,
|
||||||
h: dims + 2 * extrude,
|
h: dims,
|
||||||
originalW: aggregateOverlaySize,
|
originalW: aggregateOverlaySize,
|
||||||
originalH: aggregateOverlaySize,
|
originalH: aggregateOverlaySize,
|
||||||
|
pixelAligned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
parameters.context.imageSmoothingEnabled = true;
|
parameters.context.imageSmoothingEnabled = true;
|
||||||
|
|||||||
@ -533,7 +533,13 @@ export class BeltSystem extends GameSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Culling happens within the static map entity component
|
// 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 {
|
} else {
|
||||||
@ -544,7 +550,13 @@ export class BeltSystem extends GameSystem {
|
|||||||
const sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT];
|
const sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT];
|
||||||
|
|
||||||
// Culling happens within the static map entity component
|
// 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) *
|
((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) *
|
||||||
globalConfig.itemSpacingOnBelts
|
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);
|
parameters.context.rotate(angleRadians);
|
||||||
this.underlayBeltSprites[
|
this.underlayBeltSprites[
|
||||||
animationIndex % this.underlayBeltSprites.length
|
animationIndex % this.underlayBeltSprites.length
|
||||||
].drawCachedWithClipRect(
|
].drawCachedWithClipRect(parameters, x1, y1, x2 - x1, y2 - y1, clipRect);
|
||||||
parameters,
|
parameters.context.setTransform(transform);
|
||||||
-globalConfig.halfTileSize,
|
|
||||||
-globalConfig.halfTileSize,
|
|
||||||
globalConfig.tileSize,
|
|
||||||
globalConfig.tileSize,
|
|
||||||
clipRect
|
|
||||||
);
|
|
||||||
parameters.context.rotate(-angleRadians);
|
|
||||||
parameters.context.translate(-x, -y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export class MapResourcesSystem extends GameSystem {
|
|||||||
h: globalConfig.mapChunkWorldSize,
|
h: globalConfig.mapChunkWorldSize,
|
||||||
originalW: globalConfig.mapChunkSize,
|
originalW: globalConfig.mapChunkSize,
|
||||||
originalH: globalConfig.mapChunkSize,
|
originalH: globalConfig.mapChunkSize,
|
||||||
|
pixelAligned: true,
|
||||||
});
|
});
|
||||||
parameters.context.imageSmoothingEnabled = true;
|
parameters.context.imageSmoothingEnabled = true;
|
||||||
|
|
||||||
|
|||||||
@ -617,7 +617,7 @@ export class WireSystem extends GameSystem {
|
|||||||
assert(sprite, "Unknown wire type: " + wireType);
|
assert(sprite, "Unknown wire type: " + wireType);
|
||||||
const staticComp = entity.components.StaticMapEntity;
|
const staticComp = entity.components.StaticMapEntity;
|
||||||
parameters.context.globalAlpha = opacity;
|
parameters.context.globalAlpha = opacity;
|
||||||
staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 0);
|
staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 0, null, true);
|
||||||
|
|
||||||
// DEBUG Rendering
|
// DEBUG Rendering
|
||||||
if (G_IS_DEV && globalConfig.debug.renderWireRotations) {
|
if (G_IS_DEV && globalConfig.debug.renderWireRotations) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user