slint/examples/slide_puzzle/slide_puzzle.slint

453 lines
16 KiB
Text

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
struct Piece {
// col/row position of the tile in the puzzle
pos-x: int,
pos-y: int,
// offset in pixel from the base position for the kicking animation
offset-x: length,
offset-y: length,
}
struct Theme {
name: string,
window-background-color: brush,
game-background-color: brush,
game-use-background-image: bool,
game-border: length,
game-radius: length,
game-text-color: color,
game-highlight-color: color,
piece-border: length,
piece-background-1: brush,
piece-background-2: brush,
piece-border-color-1: brush,
piece-border-color-2: brush,
piece-text-color-1: color,
piece-text-color-2: color,
piece-text-weight-incorrect-pos: int,
piece-text-weight-correct-pos: int,
piece-text-font-family: string,
piece-radius: length,
/// Ratio of the piece size
piece-spacing: float,
}
component Checkbox inherits Rectangle {
in property <color> checked-color;
in property <color> unchecked-color;
in-out property <bool> checked;
callback toggled(bool);
states [
/* pressed when ta.pressed : {
clip.width: root.width;
root.border-color: checked_color;
root.border-width: root.width;
}*/
checked when root.checked : {
clip.width: root.width;
checkbox-rect.border-color: root.checked-color;
checkbox-rect.border-width: root.width;
in {
animate clip.width { duration: 200ms; easing: ease-in; }
animate checkbox-rect.border-width { duration: 100ms; easing: ease-out; }
}
out {
animate clip.width { duration: 100ms; easing: ease; }
animate checkbox-rect.border-width { duration: 200ms; easing: ease-in-out; }
animate checkbox-rect.border-color { duration: 200ms; easing: cubic-bezier(1,1,1,0); }
}
}
]
hover-rect := Rectangle {
background: #f5f5f5;
x: - parent.width / 4;
y: - parent.height / 4;
width: ta.has-hover ? root.width * 1.5 : 0px;
height: self.width;
border-radius: self.width;
}
checkbox-rect := Rectangle {
border-width: self.height * 10%;
border-color: root.unchecked-color;
border-radius: 2px;
clip := Rectangle {
x:0;
width: 0px;
clip: true;
Text {
x:0;y:0;
width: root.width;
height: root.height;
text: "✓";
font-size: self.height * 80%;
color: white;
vertical-alignment: center;
horizontal-alignment: center;
animate color { duration: 200ms; }
}
}
ta := TouchArea {
clicked => {
root.checked = !root.checked;
root.toggled(root.checked);
}
}
}
}
import "./plaster-font/Plaster-Regular.ttf";
export component MainWindow inherits Window {
in property <[Piece]> pieces: [
{ pos-x: 0, pos-y: 0 },
{ pos-x: 0, pos-y: 1 },
{ pos-x: 0, pos-y: 2 },
{ pos-x: 0, pos-y: 3 },
{ pos-x: 1, pos-y: 0 },
{ pos-x: 1, pos-y: 1 },
{ pos-x: 1, pos-y: 2 },
{ pos-x: 1, pos-y: 3 },
{ pos-x: 2, pos-y: 0 },
{ pos-x: 2, pos-y: 1 },
{ pos-x: 2, pos-y: 2 },
{ pos-x: 2, pos-y: 3 },
{ pos-x: 3, pos-y: 0 },
{ pos-x: 3, pos-y: 1 },
{ pos-x: 3, pos-y: 2 },
];
out property <int> current-theme-index;
in-out property <bool> auto-play;
in-out property <int> moves;
in-out property <int> tiles-left;
callback piece-clicked(int);
callback reset();
callback enable-auto-mode(bool);
private property <[Theme]> themes: [
{
name: "SIMPLE",
window-background-color: #ffffff,
game-background-color: #ffffff,
game-use-background-image: false,
game-border: 1px,
game-radius: 2px,
game-text-color: #858585,
game-highlight-color: #1d6aaa,
piece-border: 1px,
piece-background-1: #0d579b,
piece-background-2: #0d579b,
piece-border-color-1: #0a457b,
piece-border-color-2: #0a457b,
piece-text-color-1: #ffffff,
piece-text-color-2: #ffffff,
piece-text-weight-incorrect-pos: 400,
piece-text-weight-correct-pos: 700,
piece-radius: 5px,
/// Ratio of the piece size
piece-spacing: 10%,
},
{
name: "BERLIN",
window-background-color: #ffffff88,
game-background-color: #ffffffcc,
game-use-background-image: true,
game-border: 0px,
game-radius: 2px,
game-text-color: #858585,
game-highlight-color: #1d6aaa,
piece-border: 0px,
piece-background-1: #43689e,
piece-background-2: #2f2a14,
piece-border-color-1: #0000,
piece-border-color-2: #0000,
piece-text-color-1: #000000,
piece-text-color-2: #ffffff,
piece-text-weight-incorrect-pos: 700,
piece-text-weight-correct-pos: 700,
piece-radius: 0px,
/// Ratio of the piece size
piece-spacing: 8%,
},
{
name: "PLASTER",
window-background-color: #424244,
game-background-color: #f8f4e9,
game-use-background-image: false,
game-border: 5px,
game-radius: 20px,
game-text-color: #858585,
game-highlight-color: #e06b53,
piece-border: 4px,
piece-background-1: #e06b53,
piece-background-2: #f8f4e9,
piece-border-color-1: #424244,
piece-border-color-2: #e06b53,
piece-text-color-1: #f8f4e9,
piece-text-color-2: #424244,
piece-text-weight-incorrect-pos: 700,
piece-text-weight-correct-pos: 700,
piece-text-font-family: "Plaster",
piece-radius: 5px,
/// Ratio of the piece size
piece-spacing: 10%,
},
];
private property <Theme> current-theme: root.themes[root.current-theme-index];
private property <length> pieces-size: min(root.width, root.height) / 6;
private property <length> pieces-spacing: root.current-theme.game-use-background-image && root.tiles-left == 0 ?
2px : (root.pieces-size * root.current-theme.piece-spacing);
title: "Slide Puzzle - Slint Demo";
animate pieces-spacing { duration: 500ms; easing: ease-out; }
Image {
// For the wasm build we want the puzzle to resize with the browser viewport, as per CSS in index.html.
// Our winit backend preserves the CSS set size if there's no preferred size set on the Slint window.
// This image propagates its preferred size and that means the window won't scale. By positioning it
// manually, the preferred size is ignored.
x: 0; y: 0;
height: 100%; width: 100%;
// https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0
source: @image-url("berlin.jpg");
image-fit: cover;
}
Rectangle {
background: root.current-theme.window-background-color;
animate background { duration: 500ms; easing: ease-out; }
}
Rectangle {
background: root.current-theme.game-background-color;
border-color: root.current-theme.game-text-color;
border-width: root.current-theme.game-border;
border-radius: root.current-theme.game-radius;
width: root.pieces-size * 4.6;
height: root.pieces-size * 5.4;
x: (parent.width - self.width)/2;
y: (parent.height - self.height)/2;
animate background, border-color, border-width, border-radius { duration: 500ms; easing: ease-out; }
Rectangle {
y:0;
width: parent.width * 90%;
height: root.pieces-size/2;
x: (parent.width - self.width) / 2;
HorizontalLayout {
spacing: 0px;
for theme[idx] in root.themes: TouchArea {
t := Text {
width: 100%; height: 100%;
text: theme.name;
color: idx == root.current-theme-index ? root.current-theme.game-highlight-color : root.current-theme.game-text-color;
vertical-alignment: center;
horizontal-alignment: center;
}
Rectangle {
background: t.color;
height: idx == root.current-theme-index ? 2px: 1px;
y: parent.height - self.height;
}
clicked => {
root.current-theme = theme;
root.current-theme-index = idx;
}
}
}
}
for p[i] in root.pieces : Rectangle {
property <float> px: p.pos-x;
property <float> py: p.pos-y;
property <bool> is-correct: i == p.pos-x * 4 + p.pos-y;
x: self.py * (root.pieces-size + root.pieces-spacing) + p.offset-x
+ (parent.width - (4*root.pieces-size + 3*root.pieces-spacing))/2;
y: self.px * (root.pieces-size + root.pieces-spacing) + p.offset-y
+ (parent.height - (4*root.pieces-size + 3*root.pieces-spacing))/2;
width: root.pieces-size;
height: root.pieces-size;
drop-shadow-offset-x: 1px;
drop-shadow-offset-y: 1px;
drop-shadow-blur: 3px;
drop-shadow-color: #00000040;
border-radius: root.current-theme.piece-radius;
clip: true;
states [
pressed when touch.pressed : {
shadow.color: #0002;
circle.width: shadow.width * 2 * 1.4142;
in {
animate shadow.color { duration: 50ms; }
animate circle.width { duration: 2s; easing: ease-out; }
}
out {
animate shadow.color { duration: 50ms; }
}
}
hover when touch.has-hover: {
shadow.color: #0000000d;
}
]
animate px , py { duration: 170ms; easing: cubic-bezier(0.17,0.76,0.4,1.75); }
if (root.current-theme.game-use-background-image) : Image {
height: 100%; width: 100%;
// https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0
source: @image-url("berlin.jpg");
source-clip-x: mod(i, 4) * self.source.width / 4;
source-clip-y: floor(i / 4) * self.source.height / 4;
source-clip-width: self.source.width / 4;
source-clip-height: self.source.height / 4;
if (root.tiles-left != 0) : Rectangle {
width: 60%;
height: 60%;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
border-radius: self.width;
background: is-correct ? #0008 : #fff8;
}
}
if (!root.current-theme.game-use-background-image) : Rectangle {
background: i >= 8 ? root.current-theme.piece-background-2 : root.current-theme.piece-background-1;
border-color: i >= 8 ? root.current-theme.piece-border-color-2 : root.current-theme.piece-border-color-1;
border-width: root.current-theme.piece-border;
border-radius: root.current-theme.piece-radius;
animate border-width, border-radius { duration: 500ms; easing: ease-out; }
}
if (!root.current-theme.game-use-background-image || root.tiles-left > 0) : Text {
text: i+1;
color: ((!root.current-theme.game-use-background-image && i >= 8) || (root.current-theme.game-use-background-image && is-correct)) ? root.current-theme.piece-text-color-2 : root.current-theme.piece-text-color-1;
font-size: root.pieces-size / 3;
font-weight: is-correct ? root.current-theme.piece-text-weight-correct-pos : root.current-theme.piece-text-weight-incorrect-pos;
font-family: root.current-theme.piece-text-font-family;
vertical-alignment: center;
horizontal-alignment: center;
width: 100%;
height: 100%;
}
touch := TouchArea {
clicked => { root.piece-clicked(i); }
}
shadow := Rectangle {
circle := Rectangle {
height: self.width;
border-radius: self.width/2;
background: #0002;
x: touch.pressed-x - self.width/2;
y: touch.pressed-y - self.width/2;
}
}
}
if (root.tiles-left == 0) : Text {
width: root.pieces-size;
height: root.pieces-size;
x: 3 * (root.pieces-size + root.pieces-spacing)
+ (parent.width - (4*root.pieces-size + 3*root.pieces-spacing))/2;
y: 3 * (root.pieces-size + root.pieces-spacing)
+ (parent.height - (4*root.pieces-size + 3*root.pieces-spacing))/2;
color: root.current-theme.game-highlight-color;
font-size: root.pieces-size / 2;
vertical-alignment: center;
horizontal-alignment: center;
text: "🖒";
if (root.current-theme.game-use-background-image) : Image {
height: 100%; width: 100%;
// https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0
source: @image-url("berlin.jpg");
source-clip-x: 3 * self.source.width / 4;
source-clip-y: 3 * self.source.height / 4;
source-clip-width: self.source.width / 4;
source-clip-height: self.source.height / 4;
}
}
Rectangle {
width: parent.width;
height: 1px;
background: root.current-theme.game-text-color;
y: parent.height - root.pieces-size / 2;
}
HorizontalLayout {
height: root.pieces-size / 2;
y: parent.height - root.pieces-size / 2;
width: parent.width;
padding: self.height * 25%;
Text {
text: " ↻ ";
font-size: parent.height * 40%;
color: root.current-theme.game-highlight-color;
vertical-alignment: center;
TouchArea {
clicked => { root.reset(); }
}
}
Checkbox {
toggled(checked) => { root.enable-auto-mode(self.checked) }
width: parent.height - 2 * parent.padding;
checked <=> root.auto-play;
checked-color: root.current-theme.game-highlight-color;
unchecked-color: root.current-theme.game-text-color;
}
Rectangle {} // stretch
Text {
text: root.moves;
color: root.current-theme.game-highlight-color;
vertical-alignment: center;
}
Text {
text: "Moves ";
color: root.current-theme.game-text-color;
vertical-alignment: center;
}
Text {
text: root.tiles-left;
color: root.current-theme.game-highlight-color;
vertical-alignment: center;
}
Text {
text: "Tiles left";
color: root.current-theme.game-text-color;
vertical-alignment: center;
}
}
}
}