Initial commit
@@ -0,0 +1,52 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
|
||||
- name: Root pub get
|
||||
run: flutter pub get
|
||||
|
||||
- name: Example pub get
|
||||
working-directory: example
|
||||
run: flutter pub get
|
||||
|
||||
- name: Format check
|
||||
run: dart format --output=none --set-exit-if-changed .
|
||||
|
||||
- name: Analyze root
|
||||
run: flutter analyze
|
||||
|
||||
- name: Tests
|
||||
run: flutter test
|
||||
|
||||
- name: Outdated root
|
||||
run: flutter pub outdated
|
||||
|
||||
- name: Outdated example
|
||||
working-directory: example
|
||||
run: flutter pub outdated
|
||||
|
||||
- name: Dartdoc dry-run
|
||||
run: dart doc --dry-run
|
||||
|
||||
- name: Publish dry-run
|
||||
run: dart pub publish --dry-run
|
||||
|
||||
- name: Pana
|
||||
run: |
|
||||
dart pub global activate pana
|
||||
pana .
|
||||
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
- platform: android
|
||||
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "shiny",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "shiny (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "shiny (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
},
|
||||
{
|
||||
"name": "example",
|
||||
"cwd": "example",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "example (profile mode)",
|
||||
"cwd": "example",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "example (release mode)",
|
||||
"cwd": "example",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial release of `holo_shiny`.
|
||||
- Added `Shiny` widget with shader-driven holographic effects.
|
||||
- Added optional `ShinyController` and `SensorTiltController` motion APIs.
|
||||
- Added package example app and baseline tests.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,142 @@
|
||||
# holo_shiny
|
||||
|
||||
`holo_shiny` is a Flutter package that renders shader-driven holographic shine.
|
||||
|
||||
It exposes two widget layers:
|
||||
|
||||
- `Shiny`: apply shine to any widget (no clipping and no rotation)
|
||||
- `ShinyCard`: card-oriented convenience widget with shape clipping and tilt rotation
|
||||
|
||||
## Features
|
||||
|
||||
- GLSL shader-driven holographic effects
|
||||
- Optional motion input via sensors or custom tilt stream
|
||||
- Generic wrapper API for any widget via `Shiny(child: ...)`
|
||||
- Card API with `background`, `foreground`, `shape`, and drag tilt via `ShinyCard`
|
||||
- Built-in sparkle presets including 8-point star, 5-point star, rectangle, diamond, hexagon, random polygon, and confetti
|
||||
- Custom sparkle shapes via parameterized `SparkleShapeSpec` factories
|
||||
- Cross-platform Flutter support (mobile, web, desktop)
|
||||
|
||||
## Installation
|
||||
|
||||
Add the package:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
holo_shiny: ^0.1.0
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:holo_shiny/holo_shiny.dart';
|
||||
|
||||
class Demo extends StatelessWidget {
|
||||
const Demo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Shiny(
|
||||
child: ColoredBox(
|
||||
color: Color(0xFF202A3A),
|
||||
child: SizedBox(width: 280, height: 80),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Card Usage
|
||||
|
||||
```dart
|
||||
ShinyCard(
|
||||
controller: controller,
|
||||
background: Container(color: const Color(0xFF1B2D4B)),
|
||||
foreground: const Center(child: Text('HOLO')),
|
||||
sparkleShape: SparkleShapeSpec.eightPointStar,
|
||||
)
|
||||
```
|
||||
|
||||
## Motion with Controller
|
||||
|
||||
```dart
|
||||
final ShinyController controller = ShinyController(useSensor: true);
|
||||
|
||||
Shiny(
|
||||
controller: controller,
|
||||
child: Container(color: const Color(0xFF1B2D4B)),
|
||||
)
|
||||
```
|
||||
|
||||
## Sparkle Shapes
|
||||
|
||||
Use built-in sparkle presets:
|
||||
|
||||
```dart
|
||||
ShinyCard(
|
||||
sparkleShape: SparkleShapeSpec.hexagon,
|
||||
)
|
||||
```
|
||||
|
||||
Or create your own parameterized shapes:
|
||||
|
||||
```dart
|
||||
ShinyCard(
|
||||
sparkleShape: SparkleShapeSpec.customStar(
|
||||
points: 7,
|
||||
innerRatio: 0.36,
|
||||
),
|
||||
)
|
||||
|
||||
Shiny(
|
||||
sparkleShape: SparkleShapeSpec.customPolygon(
|
||||
sides: 8,
|
||||
aspectRatio: 1.2,
|
||||
rotation: 0.2,
|
||||
),
|
||||
child: const SizedBox(width: 240, height: 120),
|
||||
)
|
||||
```
|
||||
|
||||
You can also provide a custom stream:
|
||||
|
||||
```dart
|
||||
final StreamController<Offset> input = StreamController<Offset>.broadcast();
|
||||
final ShinyController controller = ShinyController(tiltStream: input.stream);
|
||||
```
|
||||
|
||||
## API Summary
|
||||
|
||||
- `Shiny`: generic effect wrapper
|
||||
- `ShinyCard`: shape + rotation + composition convenience widget
|
||||
- `SparkleShapeSpec`: built-in and custom sparkle silhouette configuration
|
||||
- `ShinyController`: optional source selection for tilt
|
||||
- `SensorTiltController`: low-level sensor fusion stream utility
|
||||
|
||||
## Platform Notes
|
||||
|
||||
- Shader uses Flutter runtime effects and avoids derivative functions (`dFdx`, `dFdy`, `fwidth`) for web compatibility.
|
||||
- If a sensor is unavailable, motion input simply emits no events.
|
||||
|
||||
## Example
|
||||
|
||||
See the package example app in `example/` for:
|
||||
|
||||
- Basic usage
|
||||
- Any-widget shiny wrapper usage
|
||||
- Sensor-driven motion
|
||||
- External stream override
|
||||
- Custom card shape/background/foreground composition
|
||||
- Built-in sparkle shape toggles in the demo UI
|
||||
- User-defined sparkle shape presets using `SparkleShapeSpec`
|
||||
|
||||
## Publishing Notes
|
||||
|
||||
Replace placeholder package metadata URLs in `pubspec.yaml` before publishing.
|
||||
@@ -0,0 +1 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.holo_shiny"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.holo_shiny"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="holo_shiny"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.holo_shiny
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
- platform: android
|
||||
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,3 @@
|
||||
# holo_shiny_example
|
||||
|
||||
A new Flutter project.
|
||||
@@ -0,0 +1 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.holo_shiny_example"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.holo_shiny_example"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="holo_shiny_example"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.holo_shiny_example
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
After Width: | Height: | Size: 300 KiB |
@@ -0,0 +1,418 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:holo_shiny/holo_shiny.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ExampleApp());
|
||||
}
|
||||
|
||||
class ExampleApp extends StatelessWidget {
|
||||
const ExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF24A6A8)),
|
||||
),
|
||||
home: const ExampleHome(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExampleHome extends StatefulWidget {
|
||||
const ExampleHome({super.key});
|
||||
|
||||
@override
|
||||
State<ExampleHome> createState() => _ExampleHomeState();
|
||||
}
|
||||
|
||||
class _ExampleHomeState extends State<ExampleHome> {
|
||||
late final ShinyController _sensorController;
|
||||
late final StreamController<Offset> _externalTiltController;
|
||||
late final ShinyController _overrideController;
|
||||
|
||||
double _prismatic = 0.8;
|
||||
double _sparkle = 0.8;
|
||||
double _specular = 0.8;
|
||||
double _diffraction = 0.8;
|
||||
HolographStyle _style = HolographStyle.crackedIce;
|
||||
SparkleShapeSpec _sparkleShape = SparkleShapeSpec.eightPointStar;
|
||||
|
||||
static const Map<String, SparkleShapeSpec> _sparkleChoices =
|
||||
<String, SparkleShapeSpec>{
|
||||
'8-Point Star': SparkleShapeSpec.eightPointStar,
|
||||
'5-Point Star': SparkleShapeSpec.fivePointStar,
|
||||
'Rectangle': SparkleShapeSpec.rectangle,
|
||||
'Diamond': SparkleShapeSpec.diamond,
|
||||
'Hexagon': SparkleShapeSpec.hexagon,
|
||||
'Random Polygon': SparkleShapeSpec.randomPolygon,
|
||||
'Confetti': SparkleShapeSpec.confetti,
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sensorController = ShinyController(useSensor: true);
|
||||
_externalTiltController = StreamController<Offset>.broadcast();
|
||||
_overrideController =
|
||||
ShinyController(tiltStream: _externalTiltController.stream);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sensorController.dispose();
|
||||
_overrideController.dispose();
|
||||
_externalTiltController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0D121A),
|
||||
appBar: AppBar(
|
||||
title: const Text('holo_shiny example'),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
const Text('Shiny on Any Widget'),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: 320,
|
||||
height: 120,
|
||||
child: Shiny(
|
||||
controller: _sensorController,
|
||||
style: _style,
|
||||
sparkleShape: _sparkleShape,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF18263E),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: const Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.auto_awesome, color: Colors.white),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'This can wrap any widget, including app chrome.',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text('ShinyCard (shape + rotation + shine)'),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: ShinyCard(
|
||||
controller: _sensorController,
|
||||
style: _style,
|
||||
sparkleShape: _sparkleShape,
|
||||
background: const _DemoCardBackground(),
|
||||
foreground: const _RareBadge(),
|
||||
prismatic: _prismatic,
|
||||
sparkle: _sparkle,
|
||||
specular: _specular,
|
||||
diffraction: _diffraction,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_StylePicker(
|
||||
selectedStyle: _style,
|
||||
onChanged: (HolographStyle style) {
|
||||
setState(() {
|
||||
_style = style;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SparkleShapePicker(
|
||||
selectedShape: _sparkleShape,
|
||||
choices: _sparkleChoices,
|
||||
onChanged: (SparkleShapeSpec shape) {
|
||||
setState(() {
|
||||
_sparkleShape = shape;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_LabeledSlider('Prismatic', _prismatic, (double value) {
|
||||
setState(() {
|
||||
_prismatic = value;
|
||||
});
|
||||
}),
|
||||
_LabeledSlider('Sparkle', _sparkle, (double value) {
|
||||
setState(() {
|
||||
_sparkle = value;
|
||||
});
|
||||
}),
|
||||
_LabeledSlider('Specular', _specular, (double value) {
|
||||
setState(() {
|
||||
_specular = value;
|
||||
});
|
||||
}),
|
||||
_LabeledSlider('Diffraction', _diffraction, (double value) {
|
||||
setState(() {
|
||||
_diffraction = value;
|
||||
});
|
||||
}),
|
||||
const SizedBox(height: 24),
|
||||
const Text('External Tilt Stream + Custom Shape (card)'),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
_externalTiltController.add(const Offset(0.7, 0.0)),
|
||||
child: const Text('Tilt Right'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
_externalTiltController.add(const Offset(-0.7, 0.0)),
|
||||
child: const Text('Tilt Left'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: ShinyCard(
|
||||
controller: _overrideController,
|
||||
style: _style,
|
||||
sparkleShape: _sparkleShape,
|
||||
shape: const StadiumBorder(),
|
||||
background: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: <Color>[Color(0xFF1A2A44), Color(0xFF15263D)],
|
||||
),
|
||||
),
|
||||
),
|
||||
foreground: const Center(
|
||||
child: Text(
|
||||
'EXTERNAL STREAM',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LabeledSlider extends StatelessWidget {
|
||||
const _LabeledSlider(this.label, this.value, this.onChanged);
|
||||
|
||||
final String label;
|
||||
final double value;
|
||||
final ValueChanged<double> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(label),
|
||||
Slider(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StylePicker extends StatelessWidget {
|
||||
const _StylePicker({
|
||||
required this.selectedStyle,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final HolographStyle selectedStyle;
|
||||
final ValueChanged<HolographStyle> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('Style'),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: HolographStyle.values.map((HolographStyle style) {
|
||||
return ChoiceChip(
|
||||
label: Text(_styleLabel(style)),
|
||||
selected: selectedStyle == style,
|
||||
onSelected: (bool selected) {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
onChanged(style);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _styleLabel(HolographStyle style) {
|
||||
switch (style) {
|
||||
case HolographStyle.holographicSilver:
|
||||
return 'Holographic Silver';
|
||||
case HolographStyle.crackedIce:
|
||||
return 'Cracked Ice';
|
||||
case HolographStyle.silverMosaic:
|
||||
return 'Silver Mosaic';
|
||||
case HolographStyle.superGoldVinyl:
|
||||
return 'Super Gold Vinyl';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SparkleShapePicker extends StatelessWidget {
|
||||
const _SparkleShapePicker({
|
||||
required this.selectedShape,
|
||||
required this.choices,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final SparkleShapeSpec selectedShape;
|
||||
final Map<String, SparkleShapeSpec> choices;
|
||||
final ValueChanged<SparkleShapeSpec> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('Sparkle Shape'),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children:
|
||||
choices.entries.map((MapEntry<String, SparkleShapeSpec> entry) {
|
||||
return ChoiceChip(
|
||||
label: Text(entry.key),
|
||||
selected: selectedShape == entry.value,
|
||||
onSelected: (bool selected) {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
onChanged(entry.value);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DemoCardBackground extends StatelessWidget {
|
||||
const _DemoCardBackground();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
Image.asset(
|
||||
'assets/pokemon.png',
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
errorBuilder:
|
||||
(BuildContext context, Object error, StackTrace? stackTrace) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[Color(0xFF2D1B1B), Color(0xFF120D18)],
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: const Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(
|
||||
'Demo Card',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Colors.black.withValues(alpha: 0.10),
|
||||
Colors.black.withValues(alpha: 0.22),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RareBadge extends StatelessWidget {
|
||||
const _RareBadge();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
child: const Text(
|
||||
'RARE',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
flutter/ephemeral
|
||||
@@ -0,0 +1,128 @@
|
||||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "holo_shiny_example")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.holo_shiny_example")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Define build configuration options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||
install(FILES "${bundled_library}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
@@ -0,0 +1,88 @@
|
||||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
|
||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||
# which isn't available in 3.10.
|
||||
function(list_prepend LIST_NAME PREFIX)
|
||||
set(NEW_LIST "")
|
||||
foreach(element ${${LIST_NAME}})
|
||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||
endforeach(element)
|
||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# === Flutter Library ===
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"fl_basic_message_channel.h"
|
||||
"fl_binary_codec.h"
|
||||
"fl_binary_messenger.h"
|
||||
"fl_dart_project.h"
|
||||
"fl_engine.h"
|
||||
"fl_json_message_codec.h"
|
||||
"fl_json_method_codec.h"
|
||||
"fl_message_codec.h"
|
||||
"fl_method_call.h"
|
||||
"fl_method_channel.h"
|
||||
"fl_method_codec.h"
|
||||
"fl_method_response.h"
|
||||
"fl_plugin_registrar.h"
|
||||
"fl_plugin_registry.h"
|
||||
"fl_standard_message_codec.h"
|
||||
"fl_standard_method_codec.h"
|
||||
"fl_string_codec.h"
|
||||
"fl_value.h"
|
||||
"fl_view.h"
|
||||
"flutter_linux.h"
|
||||
)
|
||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||
target_link_libraries(flutter INTERFACE
|
||||
PkgConfig::GTK
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GIO
|
||||
)
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
@@ -0,0 +1,23 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
@@ -0,0 +1,26 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the application ID.
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Add dependency libraries. Add any application-specific dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Called when first Flutter frame received.
|
||||
static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "holo_shiny_example");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "holo_shiny_example");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(
|
||||
project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
GdkRGBA background_color;
|
||||
// Background defaults to black, override it here if necessary, e.g. #00000000
|
||||
// for transparent.
|
||||
gdk_rgba_parse(&background_color, "#000000");
|
||||
fl_view_set_background_color(view, &background_color);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
// Show the window when Flutter renders.
|
||||
// Requires the view to be realized so we can start rendering.
|
||||
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
||||
self);
|
||||
gtk_widget_realize(GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application,
|
||||
gchar*** arguments,
|
||||
int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GApplication::startup.
|
||||
static void my_application_startup(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application startup.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||
}
|
||||
|
||||
// Implements GApplication::shutdown.
|
||||
static void my_application_shutdown(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application shutdown.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line =
|
||||
my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
// Set the program name to the application ID, which helps various systems
|
||||
// like GTK and desktop environments map this running application to its
|
||||
// corresponding .desktop file. This ensures better integration by allowing
|
||||
// the application to be recognized beyond its binary name.
|
||||
g_set_prgname(APPLICATION_ID);
|
||||
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID, "flags",
|
||||
G_APPLICATION_NON_UNIQUE, nullptr));
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication,
|
||||
my_application,
|
||||
MY,
|
||||
APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
||||
@@ -0,0 +1,23 @@
|
||||
name: holo_shiny_example
|
||||
description: Example app for holo_shiny.
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.0
|
||||
flutter: ">=3.22.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
holo_shiny:
|
||||
path: ..
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'src/sensor_tilt_controller.dart';
|
||||
export 'src/shiny_controller.dart';
|
||||
export 'src/shiny_widget.dart';
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Text('Hello World!'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sensors_plus/sensors_plus.dart';
|
||||
|
||||
/// Produces a normalized tilt stream from gyroscope and accelerometer data.
|
||||
class SensorTiltController {
|
||||
/// Creates and starts the sensor controller.
|
||||
SensorTiltController({
|
||||
this.alpha = 0.98,
|
||||
this.maxTiltRadians = 0.5,
|
||||
}) : _invAlpha = 1.0 - alpha {
|
||||
_start();
|
||||
}
|
||||
|
||||
/// Complementary filter blending coefficient.
|
||||
final double alpha;
|
||||
|
||||
/// Cached inverse alpha to avoid repetitive subtractions.
|
||||
final double _invAlpha;
|
||||
|
||||
/// Physical angle that maps to normalized magnitude `1.0`.
|
||||
final double maxTiltRadians;
|
||||
|
||||
final StreamController<Offset> _controller =
|
||||
StreamController<Offset>.broadcast();
|
||||
|
||||
// High-performance timing for tight physics loops
|
||||
final Stopwatch _stopwatch = Stopwatch();
|
||||
|
||||
StreamSubscription<GyroscopeEvent>? _gyroSub;
|
||||
StreamSubscription<AccelerometerEvent>? _accelSub;
|
||||
|
||||
double _roll = 0.0;
|
||||
double _pitch = 0.0;
|
||||
double _accelRoll = 0.0;
|
||||
double _accelPitch = 0.0;
|
||||
int? _lastMicroseconds;
|
||||
|
||||
/// The normalized tilt stream.
|
||||
Stream<Offset> get stream => _controller.stream;
|
||||
|
||||
void _start() {
|
||||
if (!_supportsSensorStreams) return;
|
||||
|
||||
_stopwatch.start();
|
||||
|
||||
try {
|
||||
_gyroSub = gyroscopeEventStream().listen(
|
||||
_onGyro,
|
||||
onError: (_) {},
|
||||
cancelOnError: false,
|
||||
);
|
||||
} on MissingPluginException {
|
||||
_gyroSub = null;
|
||||
}
|
||||
|
||||
try {
|
||||
_accelSub = accelerometerEventStream().listen(
|
||||
_onAccel,
|
||||
onError: (_) {},
|
||||
cancelOnError: false,
|
||||
);
|
||||
} on MissingPluginException {
|
||||
_accelSub = null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get _supportsSensorStreams {
|
||||
if (kIsWeb) return true;
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
return true;
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _onGyro(GyroscopeEvent event) {
|
||||
final int nowMicros = _stopwatch.elapsedMicroseconds;
|
||||
final int? last = _lastMicroseconds;
|
||||
_lastMicroseconds = nowMicros;
|
||||
|
||||
if (last == null) return;
|
||||
|
||||
final double dt = (nowMicros - last) / 1000000.0;
|
||||
if (dt <= 0.0) return;
|
||||
|
||||
final double gyroRoll = _roll + (event.y * dt);
|
||||
final double gyroPitch = _pitch + (event.x * dt);
|
||||
|
||||
_roll = (alpha * gyroRoll) + (_invAlpha * _accelRoll);
|
||||
_pitch = (alpha * gyroPitch) + (_invAlpha * _accelPitch);
|
||||
|
||||
_controller.add(_normalizedOffset());
|
||||
}
|
||||
|
||||
void _onAccel(AccelerometerEvent event) {
|
||||
const double g = 9.81;
|
||||
_accelRoll = (event.x / g).clamp(-1.0, 1.0);
|
||||
_accelPitch = (event.y / g).clamp(-1.0, 1.0);
|
||||
}
|
||||
|
||||
Offset _normalizedOffset() {
|
||||
final double dx = (_roll / maxTiltRadians).clamp(-1.0, 1.0);
|
||||
final double dy = (_pitch / maxTiltRadians).clamp(-1.0, 1.0);
|
||||
return Offset(dx, dy);
|
||||
}
|
||||
|
||||
/// Stops sensor streams and releases resources.
|
||||
Future<void> dispose() async {
|
||||
_stopwatch.stop();
|
||||
await _gyroSub?.cancel();
|
||||
await _accelSub?.cancel();
|
||||
await _controller.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'sensor_tilt_controller.dart';
|
||||
|
||||
/// Optional controller for providing tilt input to [Shiny] widgets.
|
||||
///
|
||||
/// By default, no stream is emitted. Set [useSensor] to true to use built-in
|
||||
/// motion sensors, or provide [tiltStream] to fully control tilt externally.
|
||||
class ShinyController {
|
||||
/// Creates a [ShinyController].
|
||||
///
|
||||
/// If [tiltStream] is provided, it overrides the built-in sensor stream.
|
||||
ShinyController({
|
||||
this.useSensor = false,
|
||||
this.tiltStream,
|
||||
}) {
|
||||
if (tiltStream == null && useSensor) {
|
||||
_sensorController = SensorTiltController();
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables internal sensor-based tilt when [tiltStream] is null.
|
||||
final bool useSensor;
|
||||
|
||||
/// Optional external tilt stream in normalized `[-1.0, 1.0]` space.
|
||||
final Stream<Offset>? tiltStream;
|
||||
|
||||
SensorTiltController? _sensorController;
|
||||
|
||||
/// The effective tilt stream consumed by [Shiny].
|
||||
Stream<Offset> get stream {
|
||||
if (tiltStream != null) {
|
||||
return tiltStream!;
|
||||
}
|
||||
if (_sensorController != null) {
|
||||
return _sensorController!.stream;
|
||||
}
|
||||
return const Stream<Offset>.empty();
|
||||
}
|
||||
|
||||
/// Disposes internally-owned resources.
|
||||
Future<void> dispose() async {
|
||||
await _sensorController?.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'shiny_controller.dart';
|
||||
|
||||
enum HolographStyle {
|
||||
holographicSilver,
|
||||
crackedIce,
|
||||
silverMosaic,
|
||||
superGoldVinyl,
|
||||
}
|
||||
|
||||
enum SparkleShapeKind {
|
||||
star,
|
||||
rectangle,
|
||||
polygon,
|
||||
randomPolygon,
|
||||
confetti,
|
||||
}
|
||||
|
||||
class SparkleShapeSpec {
|
||||
const SparkleShapeSpec._({
|
||||
required this.kind,
|
||||
required this.primary,
|
||||
required this.secondary,
|
||||
required this.tertiary,
|
||||
});
|
||||
|
||||
static const SparkleShapeSpec eightPointStar = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.star,
|
||||
primary: 8.0,
|
||||
secondary: 0.42,
|
||||
tertiary: 0.0,
|
||||
);
|
||||
|
||||
static const SparkleShapeSpec fivePointStar = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.star,
|
||||
primary: 5.0,
|
||||
secondary: 0.42,
|
||||
tertiary: 0.0,
|
||||
);
|
||||
|
||||
static const SparkleShapeSpec rectangle = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.rectangle,
|
||||
primary: 0.24,
|
||||
secondary: 0.055,
|
||||
tertiary: 1.0,
|
||||
);
|
||||
|
||||
static const SparkleShapeSpec diamond = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.polygon,
|
||||
primary: 4.0,
|
||||
secondary: 1.0,
|
||||
tertiary: 0.78539816339,
|
||||
);
|
||||
|
||||
static const SparkleShapeSpec hexagon = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.polygon,
|
||||
primary: 6.0,
|
||||
secondary: 1.0,
|
||||
tertiary: 0.0,
|
||||
);
|
||||
|
||||
static const SparkleShapeSpec randomPolygon = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.randomPolygon,
|
||||
primary: 0.0,
|
||||
secondary: 0.0,
|
||||
tertiary: 0.0,
|
||||
);
|
||||
|
||||
static const SparkleShapeSpec confetti = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.confetti,
|
||||
primary: 0.34,
|
||||
secondary: 0.045,
|
||||
tertiary: 1.0,
|
||||
);
|
||||
|
||||
factory SparkleShapeSpec.customStar(
|
||||
{int points = 8, double innerRatio = 0.42}) {
|
||||
return SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.star,
|
||||
primary: points.toDouble().clamp(4.0, 12.0),
|
||||
secondary: innerRatio.clamp(0.15, 0.85),
|
||||
tertiary: 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
factory SparkleShapeSpec.customPolygon(
|
||||
{int sides = 6, double aspectRatio = 1.0, double rotation = 0.0}) {
|
||||
return SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.polygon,
|
||||
primary: sides.toDouble().clamp(3.0, 10.0),
|
||||
secondary: aspectRatio.clamp(0.4, 2.0),
|
||||
tertiary: rotation,
|
||||
);
|
||||
}
|
||||
|
||||
factory SparkleShapeSpec.customRectangle(
|
||||
{double halfWidth = 0.24,
|
||||
double halfHeight = 0.055,
|
||||
double rotationJitter = 1.0}) {
|
||||
return SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.rectangle,
|
||||
primary: halfWidth.clamp(0.05, 0.45),
|
||||
secondary: halfHeight.clamp(0.02, 0.30),
|
||||
tertiary: rotationJitter.clamp(0.0, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
factory SparkleShapeSpec.customConfetti(
|
||||
{double length = 0.34,
|
||||
double thickness = 0.045,
|
||||
double rotationJitter = 1.0}) {
|
||||
return SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.confetti,
|
||||
primary: length.clamp(0.08, 0.48),
|
||||
secondary: thickness.clamp(0.01, 0.14),
|
||||
tertiary: rotationJitter.clamp(0.0, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
final SparkleShapeKind kind;
|
||||
final double primary;
|
||||
final double secondary;
|
||||
final double tertiary;
|
||||
}
|
||||
|
||||
class Shiny extends StatefulWidget {
|
||||
const Shiny({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.controller,
|
||||
this.tilt,
|
||||
this.prismatic = 0.8,
|
||||
this.sparkle = 0.8,
|
||||
this.specular = 0.8,
|
||||
this.diffraction = 0.8,
|
||||
this.sparkleShape = SparkleShapeSpec.eightPointStar,
|
||||
this.style = HolographStyle.crackedIce,
|
||||
this.blendMode = BlendMode.screen,
|
||||
this.enableShader = true,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final ShinyController? controller;
|
||||
final Offset? tilt;
|
||||
final double prismatic;
|
||||
final double sparkle;
|
||||
final double specular;
|
||||
final double diffraction;
|
||||
final SparkleShapeSpec sparkleShape;
|
||||
final HolographStyle style;
|
||||
final BlendMode blendMode;
|
||||
final bool enableShader;
|
||||
|
||||
@override
|
||||
State<Shiny> createState() => _ShinyState();
|
||||
}
|
||||
|
||||
class _ShinyState extends State<Shiny> with TickerProviderStateMixin {
|
||||
static Future<ui.FragmentProgram>? _programFuture;
|
||||
|
||||
ui.FragmentShader? _shader;
|
||||
StreamSubscription<Offset>? _tiltSub;
|
||||
late final Ticker _ticker;
|
||||
|
||||
double _time = 0.0;
|
||||
Offset _tilt = Offset.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ticker = createTicker((Duration elapsed) {
|
||||
if (!mounted || _shader == null) return;
|
||||
setState(() {
|
||||
_time = elapsed.inMicroseconds / 1000000.0;
|
||||
});
|
||||
});
|
||||
|
||||
_attachController();
|
||||
if (widget.enableShader) {
|
||||
_loadShader();
|
||||
_ticker.start();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant Shiny oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller ||
|
||||
oldWidget.tilt != widget.tilt) {
|
||||
_tiltSub?.cancel();
|
||||
_attachController();
|
||||
}
|
||||
if (oldWidget.enableShader != widget.enableShader) {
|
||||
if (widget.enableShader) {
|
||||
if (_shader == null) _loadShader();
|
||||
_ticker.start();
|
||||
} else {
|
||||
_ticker.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadShader() async {
|
||||
try {
|
||||
_programFuture ??= _loadProgram();
|
||||
final ui.FragmentProgram program = await _programFuture!;
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_shader = program.fragmentShader();
|
||||
});
|
||||
} catch (_) {
|
||||
// Keep rendering without shader when runtime effects are unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
Future<ui.FragmentProgram> _loadProgram() async {
|
||||
try {
|
||||
return await ui.FragmentProgram.fromAsset('shaders/shiny_card.frag');
|
||||
} catch (_) {
|
||||
return await ui.FragmentProgram.fromAsset(
|
||||
'packages/holo_shiny/shaders/shiny_card.frag');
|
||||
}
|
||||
}
|
||||
|
||||
void _attachController() {
|
||||
if (widget.tilt != null) return;
|
||||
final ShinyController? controller = widget.controller;
|
||||
if (controller == null) return;
|
||||
|
||||
_tiltSub = controller.stream.listen((Offset value) {
|
||||
if (!mounted || widget.tilt != null) return;
|
||||
final clamped = _clampOffset(value);
|
||||
if (_tilt != clamped) {
|
||||
setState(() {
|
||||
_tilt = clamped;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker.dispose();
|
||||
_tiltSub?.cancel();
|
||||
_shader?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ui.Shader _configureShader(Rect bounds) {
|
||||
final Offset effectiveTilt = _clampOffset(widget.tilt ?? _tilt);
|
||||
final ui.FragmentShader shader = _shader!;
|
||||
shader
|
||||
..setFloat(0, bounds.width)
|
||||
..setFloat(1, bounds.height)
|
||||
..setFloat(2, effectiveTilt.dx)
|
||||
..setFloat(3, effectiveTilt.dy)
|
||||
..setFloat(4, _time)
|
||||
..setFloat(5, widget.prismatic)
|
||||
..setFloat(6, widget.sparkle)
|
||||
..setFloat(7, widget.specular)
|
||||
..setFloat(8, widget.diffraction)
|
||||
..setFloat(9, widget.style.index.toDouble())
|
||||
..setFloat(10, widget.sparkleShape.kind.index.toDouble())
|
||||
..setFloat(11, widget.sparkleShape.primary)
|
||||
..setFloat(12, widget.sparkleShape.secondary)
|
||||
..setFloat(13, widget.sparkleShape.tertiary);
|
||||
return shader;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.enableShader || _shader == null) return widget.child;
|
||||
|
||||
return ShaderMask(
|
||||
blendMode: widget.blendMode,
|
||||
shaderCallback: _configureShader,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
Offset _clampOffset(Offset value) {
|
||||
return Offset(value.dx.clamp(-1.0, 1.0), value.dy.clamp(-1.0, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
class ShinyCard extends StatefulWidget {
|
||||
const ShinyCard({
|
||||
super.key,
|
||||
this.background,
|
||||
this.foreground,
|
||||
this.shape = const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
this.width = 300,
|
||||
this.height = 420,
|
||||
this.controller,
|
||||
this.prismatic = 0.8,
|
||||
this.sparkle = 0.8,
|
||||
this.specular = 0.8,
|
||||
this.diffraction = 0.8,
|
||||
this.sparkleShape = SparkleShapeSpec.eightPointStar,
|
||||
this.style = HolographStyle.crackedIce,
|
||||
this.enableShader = true,
|
||||
});
|
||||
|
||||
final Widget? background;
|
||||
final Widget? foreground;
|
||||
final ShapeBorder shape;
|
||||
final double width;
|
||||
final double height;
|
||||
final ShinyController? controller;
|
||||
final double prismatic;
|
||||
final double sparkle;
|
||||
final double specular;
|
||||
final double diffraction;
|
||||
final SparkleShapeSpec sparkleShape;
|
||||
final HolographStyle style;
|
||||
final bool enableShader;
|
||||
|
||||
@override
|
||||
State<ShinyCard> createState() => _ShinyCardState();
|
||||
}
|
||||
|
||||
class _ShinyCardState extends State<ShinyCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const ValueKey<String> transformKey =
|
||||
ValueKey<String>('holo_shiny.card.transform');
|
||||
|
||||
StreamSubscription<Offset>? _tiltSub;
|
||||
late final AnimationController _spring;
|
||||
|
||||
// State segregation: prevents total widget rebuilds during drag events
|
||||
late final ValueNotifier<Offset> _tiltNotifier = ValueNotifier(Offset.zero);
|
||||
late final StreamController<Offset> _internalTiltStream =
|
||||
StreamController<Offset>.broadcast();
|
||||
late final ShinyController _internalShinyController =
|
||||
ShinyController(tiltStream: _internalTiltStream.stream);
|
||||
|
||||
Offset _sensorTilt = Offset.zero;
|
||||
Offset _dragTilt = Offset.zero;
|
||||
Offset _dragTiltAtRelease = Offset.zero;
|
||||
bool _isDragging = false;
|
||||
bool _isReturning = false;
|
||||
|
||||
void _updateTilt() {
|
||||
final tilt = (_isDragging || _isReturning) ? _dragTilt : _sensorTilt;
|
||||
if (_tiltNotifier.value != tilt) {
|
||||
_tiltNotifier.value = tilt;
|
||||
if (!_internalTiltStream.isClosed) {
|
||||
_internalTiltStream.add(tilt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_spring = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 850))
|
||||
..addListener(() {
|
||||
if (!mounted || !_isReturning) return;
|
||||
final double t = Curves.elasticOut.transform(_spring.value);
|
||||
_dragTilt =
|
||||
Offset.lerp(_dragTiltAtRelease, Offset.zero, t) ?? Offset.zero;
|
||||
_updateTilt();
|
||||
})
|
||||
..addStatusListener((AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_isReturning = false;
|
||||
_updateTilt();
|
||||
}
|
||||
});
|
||||
|
||||
_attachController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ShinyCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
_tiltSub?.cancel();
|
||||
_attachController();
|
||||
}
|
||||
}
|
||||
|
||||
void _attachController() {
|
||||
final ShinyController? controller = widget.controller;
|
||||
if (controller == null) return;
|
||||
|
||||
_tiltSub = controller.stream.listen((Offset value) {
|
||||
if (!mounted || _isDragging || _isReturning) return;
|
||||
_sensorTilt = _clampOffset(value);
|
||||
_updateTilt();
|
||||
});
|
||||
}
|
||||
|
||||
void _onPanUpdate(DragUpdateDetails details) {
|
||||
_isDragging = true;
|
||||
_isReturning = false;
|
||||
_spring.stop();
|
||||
|
||||
final double dx = (details.delta.dx / widget.width) * 2.0;
|
||||
final double dy = (details.delta.dy / widget.height) * 2.0;
|
||||
|
||||
_dragTilt = _clampOffset(_dragTilt + Offset(dx, dy));
|
||||
_updateTilt();
|
||||
}
|
||||
|
||||
void _onPanEnd(DragEndDetails details) {
|
||||
_isDragging = false;
|
||||
_isReturning = true;
|
||||
_dragTiltAtRelease = _dragTilt;
|
||||
_spring
|
||||
..reset()
|
||||
..forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tiltSub?.cancel();
|
||||
_spring.dispose();
|
||||
_tiltNotifier.dispose();
|
||||
_internalTiltStream.close();
|
||||
_internalShinyController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget base = widget.background ??
|
||||
const ColoredBox(color: Color(0xFF212121), child: SizedBox.expand());
|
||||
|
||||
// Isolate the static tree structure so it never rebuilds when tilted
|
||||
final Widget staticChild = ClipPath(
|
||||
clipper: ShapeBorderClipper(shape: widget.shape),
|
||||
child: SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
Shiny(
|
||||
enableShader: widget.enableShader,
|
||||
controller: _internalShinyController,
|
||||
tilt: null, // Managed natively via the internal stream controller
|
||||
prismatic: widget.prismatic,
|
||||
sparkle: widget.sparkle,
|
||||
specular: widget.specular,
|
||||
diffraction: widget.diffraction,
|
||||
sparkleShape: widget.sparkleShape,
|
||||
style: widget.style,
|
||||
child: base,
|
||||
),
|
||||
if (widget.foreground != null) widget.foreground!,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onPanUpdate: _onPanUpdate,
|
||||
onPanEnd: _onPanEnd,
|
||||
child: ValueListenableBuilder<Offset>(
|
||||
valueListenable: _tiltNotifier,
|
||||
builder: (context, tilt, child) {
|
||||
final Matrix4 transform = Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateX(-tilt.dy * 0.3)
|
||||
..rotateY(tilt.dx * 0.3);
|
||||
|
||||
return Transform(
|
||||
key: transformKey,
|
||||
alignment: Alignment.center,
|
||||
transform: transform,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: staticChild,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Offset _clampOffset(Offset value) {
|
||||
return Offset(value.dx.clamp(-1.0, 1.0), value.dy.clamp(-1.0, 1.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
name: holo_shiny
|
||||
description: Holographic shiny card widget powered by GLSL fragment shaders.
|
||||
version: 0.1.0
|
||||
publish_to: none
|
||||
repository: https://example.com/your-org/holo_shiny
|
||||
homepage: https://example.com/your-org/holo_shiny
|
||||
issue_tracker: https://example.com/your-org/holo_shiny/issues
|
||||
documentation: https://example.com/your-org/holo_shiny/docs
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.0
|
||||
flutter: ">=3.22.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
sensors_plus: ^7.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
shaders:
|
||||
- shaders/shiny_card.frag
|
||||
@@ -0,0 +1,378 @@
|
||||
#version 460 core
|
||||
#include <flutter/runtime_effect.glsl>
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform vec2 uSize;
|
||||
uniform vec2 uTilt;
|
||||
uniform float uTime;
|
||||
uniform float uPrismatic;
|
||||
uniform float uSparkle;
|
||||
uniform float uSpecular;
|
||||
uniform float uDiffraction;
|
||||
uniform float uStyle;
|
||||
uniform float uSparkleShapeKind;
|
||||
uniform float uSparklePrimary;
|
||||
uniform float uSparkleSecondary;
|
||||
uniform float uSparkleTertiary;
|
||||
|
||||
#define TWO_PI 6.28318530718
|
||||
|
||||
// Precomputed rotation matrices to avoid expensive runtime sin/cos calculations
|
||||
#define ROT_0_78 mat2( 0.71091, 0.70328, -0.70328, 0.71091)
|
||||
#define ROT_M0_5 mat2( 0.87758, -0.47943, 0.47943, 0.87758)
|
||||
#define ROT_1_2 mat2( 0.36236, 0.93204, -0.93204, 0.36236)
|
||||
|
||||
float hash(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
float noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float a = hash(i);
|
||||
float b = hash(i + vec2(1.0, 0.0));
|
||||
float c = hash(i + vec2(0.0, 1.0));
|
||||
float d = hash(i + vec2(1.0, 1.0));
|
||||
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
vec3 hsv2rgb(float h, float s, float v) {
|
||||
vec3 k = vec3(1.0, 2.0 / 3.0, 1.0 / 3.0);
|
||||
vec3 p = abs(fract(vec3(h) + k) * 6.0 - 3.0);
|
||||
return v * mix(vec3(1.0), clamp(p - 1.0, 0.0, 1.0), s);
|
||||
}
|
||||
|
||||
vec3 rainbow(float phase, float saturation, float value) {
|
||||
return hsv2rgb(fract(phase), saturation, value);
|
||||
}
|
||||
|
||||
float sdBox(vec2 p, vec2 b) {
|
||||
vec2 d = abs(p) - b;
|
||||
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
|
||||
}
|
||||
|
||||
float sdRegularPolygon(vec2 p, float sides, float radius) {
|
||||
float angle = atan(p.y, p.x);
|
||||
float sector = TWO_PI / sides;
|
||||
return cos(floor(0.5 + angle / sector) * sector - angle) * length(p) - radius;
|
||||
}
|
||||
|
||||
float sparkleStarMask(vec2 p, float points, float innerRatio) {
|
||||
float angle = atan(p.y, p.x);
|
||||
float radius = dot(p, p); // Optimized: Use squared distance to delay sqrt
|
||||
float spikes = 0.5 + 0.5 * cos(angle * points);
|
||||
float starRadius = mix(0.34 * innerRatio, 0.34, pow(spikes, 1.4));
|
||||
return smoothstep(0.0009, -0.0009, radius - (starRadius * starRadius));
|
||||
}
|
||||
|
||||
vec2 rotatePoint(vec2 p, float rotation) {
|
||||
float c = cos(rotation);
|
||||
float s = sin(rotation);
|
||||
return vec2(c * p.x - s * p.y, s * p.x + c * p.y);
|
||||
}
|
||||
|
||||
float sparkleRectangleMask(vec2 p, float halfWidth, float halfHeight, float rotation) {
|
||||
vec2 q = rotatePoint(p, rotation);
|
||||
float body = sdBox(q, vec2(halfWidth, halfHeight));
|
||||
float bevel = sdBox(q, vec2(halfWidth * 0.78, halfHeight * 0.78));
|
||||
float bodyMask = smoothstep(0.03, -0.03, body);
|
||||
|
||||
float centerLine = 1.0 - smoothstep(0.0, halfHeight * 1.25, abs(q.y));
|
||||
float endFade = 1.0 - smoothstep(halfWidth * 0.55, halfWidth * 1.05, abs(q.x));
|
||||
float streak = centerLine * endFade;
|
||||
float bevelRing = smoothstep(0.025, -0.025, body) - smoothstep(0.025, -0.025, bevel);
|
||||
return clamp(bodyMask * (0.58 + 0.42 * streak) + bevelRing * 0.25, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float sparklePolygonMask(vec2 p, float sides, float aspectRatio, float rotation) {
|
||||
vec2 q = rotatePoint(p, rotation);
|
||||
q.x /= aspectRatio;
|
||||
float poly = sdRegularPolygon(q, sides, 0.26);
|
||||
return smoothstep(0.03, -0.03, poly);
|
||||
}
|
||||
|
||||
float sparkleShapeMask(vec2 p, vec2 id) {
|
||||
float randomRotation = hash(id + 5.1) * TWO_PI;
|
||||
if (uSparkleShapeKind < 0.5) {
|
||||
return sparkleStarMask(p, uSparklePrimary, uSparkleSecondary);
|
||||
}
|
||||
if (uSparkleShapeKind < 1.5) {
|
||||
return sparkleRectangleMask(p, uSparklePrimary, uSparkleSecondary, randomRotation * uSparkleTertiary);
|
||||
}
|
||||
if (uSparkleShapeKind < 2.5) {
|
||||
return sparklePolygonMask(p, uSparklePrimary, uSparkleSecondary, uSparkleTertiary);
|
||||
}
|
||||
if (uSparkleShapeKind < 3.5) {
|
||||
float sides = 5.0 + floor(hash(id + 9.4) * 4.0);
|
||||
float aspect = 0.7 + hash(id + 13.1) * 0.7;
|
||||
return sparklePolygonMask(p, sides, aspect, randomRotation);
|
||||
}
|
||||
return sparkleRectangleMask(p, uSparklePrimary, uSparkleSecondary, randomRotation * uSparkleTertiary);
|
||||
}
|
||||
|
||||
vec3 styleHolographicSilver(vec2 uv, vec2 tilt, float time) {
|
||||
vec2 p = uv * 2.0 - 1.0;
|
||||
float flowA = noise(uv * 3.5 + vec2(time * 0.03, -time * 0.02));
|
||||
float flowB = noise(uv * 7.0 + vec2(-time * 0.02, time * 0.03));
|
||||
vec2 warp = p + 0.25 * vec2(flowA - 0.5, flowB - 0.5);
|
||||
float ribbon = sin(dot(warp, vec2(6.5, 2.8)) + flowA * 5.0 + dot(tilt, vec2(2.2, -1.7)));
|
||||
float plume = sin(length(warp + vec2(flowB - 0.5, flowA - 0.5)) * 9.0 - tilt.x * 3.0 + time * 0.2);
|
||||
float phase = flowA * 1.8 + ribbon * 0.28 + plume * 0.22 + time * 0.03;
|
||||
|
||||
vec3 holo = rainbow(phase, 0.72, 1.0);
|
||||
float blend = smoothstep(-0.65, 0.95, ribbon + plume * 0.55);
|
||||
vec3 silver = vec3(0.62, 0.65, 0.70) * (0.85 + 0.15 * flowB);
|
||||
|
||||
return mix(silver, holo, 0.65 * blend);
|
||||
}
|
||||
|
||||
vec3 styleCrackedIce(vec2 uv, vec2 tilt, float time) {
|
||||
float maxZ = -999.0;
|
||||
vec2 bestSlope = vec2(0.0);
|
||||
float bestHash = 0.0;
|
||||
float bestEdge = 1.0;
|
||||
|
||||
vec2 warp = vec2(sin(uv.y * 10.0), cos(uv.x * 10.0)) * 0.05;
|
||||
vec2 baseUV = uv + warp;
|
||||
|
||||
// --- LAYER 1 ---
|
||||
vec2 p1 = baseUV * 12.0;
|
||||
vec2 id1 = floor(p1);
|
||||
vec2 f1 = fract(p1) - 0.5;
|
||||
vec2 slope1 = vec2(hash(id1 + 1.2), hash(id1 + 1.3)) * 2.0 - 1.0;
|
||||
float z1 = dot(f1, slope1) * 3.5;
|
||||
if (z1 > maxZ) {
|
||||
maxZ = z1; bestSlope = slope1; bestHash = hash(id1 + 1.1); bestEdge = min(0.5 - abs(f1.x), 0.5 - abs(f1.y));
|
||||
}
|
||||
|
||||
// --- LAYER 2 (Optimized Rotation) ---
|
||||
vec2 p2 = (ROT_0_78 * baseUV) * 15.0;
|
||||
vec2 id2 = floor(p2);
|
||||
vec2 f2 = fract(p2) - 0.5;
|
||||
vec2 slope2 = vec2(hash(id2 + 2.2), hash(id2 + 2.3)) * 2.0 - 1.0;
|
||||
float z2 = 0.8 + dot(f2, slope2) * 3.5;
|
||||
if (z2 > maxZ) {
|
||||
maxZ = z2; bestSlope = slope2; bestHash = hash(id2 + 2.1); bestEdge = min(0.5 - abs(f2.x), 0.5 - abs(f2.y));
|
||||
}
|
||||
|
||||
// --- LAYER 3 (Optimized Rotation) ---
|
||||
vec2 p3 = (ROT_M0_5 * baseUV) * 18.0;
|
||||
vec2 id3 = floor(p3);
|
||||
vec2 f3 = fract(p3) - 0.5;
|
||||
vec2 slope3 = vec2(hash(id3 + 3.2), hash(id3 + 3.3)) * 2.0 - 1.0;
|
||||
float z3 = 1.6 + dot(f3, slope3) * 3.5;
|
||||
if (z3 > maxZ) {
|
||||
maxZ = z3; bestSlope = slope3; bestHash = hash(id3 + 3.1); bestEdge = min(0.5 - abs(f3.x), 0.5 - abs(f3.y));
|
||||
}
|
||||
|
||||
// --- LAYER 4 (Optimized Rotation) ---
|
||||
vec2 p4 = (ROT_1_2 * baseUV) * 21.0;
|
||||
vec2 id4 = floor(p4);
|
||||
vec2 f4 = fract(p4) - 0.5;
|
||||
vec2 slope4 = vec2(hash(id4 + 4.2), hash(id4 + 4.3)) * 2.0 - 1.0;
|
||||
float z4 = 2.4 + dot(f4, slope4) * 3.5;
|
||||
if (z4 > maxZ) {
|
||||
maxZ = z4; bestSlope = slope4; bestHash = hash(id4 + 4.1); bestEdge = min(0.5 - abs(f4.x), 0.5 - abs(f4.y));
|
||||
}
|
||||
|
||||
// --- VIBRANT COLOR OPTICS ---
|
||||
float alignment = dot(tilt, normalize(bestSlope + vec2(0.001)));
|
||||
float phase = dot(uv, vec2(0.6, -0.4)) + time * 0.05 + bestHash * 0.6 + alignment * 0.5;
|
||||
float saturation = 0.7 + 0.3 * hash(vec2(bestHash, 6.1));
|
||||
vec3 baseColor = rainbow(phase, saturation, 1.0);
|
||||
|
||||
float lightCycle = sin(alignment * 4.0 + bestHash * TWO_PI + time * 0.15);
|
||||
float ambient = 0.15 + 0.25 * max(0.0, lightCycle);
|
||||
vec3 finalColor = baseColor * ambient;
|
||||
float flash = pow(max(0.0, lightCycle), 5.0);
|
||||
finalColor += mix(baseColor, vec3(1.0, 0.95, 0.85), 0.6) * flash * 0.55;
|
||||
finalColor += baseColor * smoothstep(0.05, 0.0, bestEdge) * flash * 0.4;
|
||||
|
||||
return finalColor;
|
||||
}
|
||||
|
||||
vec3 styleSilverMosaic(vec2 uv, vec2 tilt, float time) {
|
||||
// Setup grid - Scaled up 300% (18.0 -> 6.0)
|
||||
vec2 g = uv * 6.0;
|
||||
vec2 id = floor(g);
|
||||
|
||||
// Center coordinates around 0.0 for the radial math
|
||||
vec2 f = fract(g) - 0.5;
|
||||
|
||||
// Real foil sheets often alternate the grain of the squares to catch light from all angles.
|
||||
if (mod(id.x + id.y, 2.0) > 0.5) {
|
||||
f = vec2(-f.y, f.x);
|
||||
}
|
||||
|
||||
// Get the radial vector (pointing outward from the center of the square)
|
||||
vec2 radialDir = normalize(f + vec2(0.0001));
|
||||
|
||||
// Calculate the simulated light direction based on device tilt.
|
||||
vec2 lightDir = normalize(tilt + vec2(0.001));
|
||||
|
||||
// --- THE OPTICS ---
|
||||
// A radial highlight occurs where the radial vector aligns with the light vector.
|
||||
float alignment = abs(dot(radialDir, lightDir));
|
||||
|
||||
// Raise to a high power to narrow the reflection into a sharp, focused beam
|
||||
float highlight = pow(alignment, 12.0);
|
||||
|
||||
// --- THE DIFFRACTION (Rainbow) ---
|
||||
float angleDiff = acos(alignment);
|
||||
|
||||
// 2D cross product to find which side of the highlight we are on
|
||||
float side = sign(radialDir.x * lightDir.y - radialDir.y * lightDir.x);
|
||||
|
||||
// Phase shift based on tilt magnitude and time.
|
||||
float basePhase = length(tilt) * 2.5 + time * 0.1;
|
||||
|
||||
// Calculate the final color phase.
|
||||
float phase = basePhase + side * angleDiff * 1.8;
|
||||
vec3 holoColor = rainbow(phase, 0.85, 1.0);
|
||||
|
||||
// Add a pure white "specular core" where alignment is absolutely perfect
|
||||
vec3 core = vec3(1.0) * smoothstep(0.98, 1.0, alignment);
|
||||
|
||||
// Base foil material
|
||||
vec3 foilBase = vec3(0.25, 0.27, 0.30);
|
||||
|
||||
// Combine the light components
|
||||
vec3 finalColor = foilBase + (holoColor * highlight * 1.5) + (core * 0.8);
|
||||
|
||||
// --- BORDERS AND DEPTH ---
|
||||
// Crisp borders between the squares
|
||||
float edgeX = 0.5 - abs(f.x);
|
||||
float edgeY = 0.5 - abs(f.y);
|
||||
|
||||
// Tightened the border width to compensate for the larger tile size
|
||||
float border = smoothstep(0.0, 0.015, min(edgeX, edgeY));
|
||||
|
||||
// Add a subtle darkening toward the edges of the tiles
|
||||
float dist = length(f);
|
||||
finalColor *= mix(0.7, 1.0, 1.0 - smoothstep(0.0, 0.5, dist) * 0.3);
|
||||
|
||||
return clamp(finalColor * border, 0.0, 1.0);
|
||||
}
|
||||
|
||||
vec3 styleSuperGoldVinyl(vec2 uv, vec2 tilt, float time) {
|
||||
vec2 g = uv * 60.0;
|
||||
vec2 staggered = vec2(g.x + 0.5 * mod(floor(g.y), 2.0), g.y);
|
||||
vec2 f = fract(staggered) - 0.5;
|
||||
|
||||
// Optimized: Use dot product for squared distance to avoid expensive sqrt/length.
|
||||
// Original smoothstep values (0.42, 0.10) squared become (0.1764, 0.0100)
|
||||
// Original smoothstep values (0.45, 0.18) squared become (0.2025, 0.0324)
|
||||
float d2 = dot(f, f);
|
||||
float dotMask = smoothstep(0.1764, 0.01, d2);
|
||||
float emboss = smoothstep(0.2025, 0.0324, d2);
|
||||
|
||||
float sheen = 0.5 + 0.5 * sin(dot(uv, vec2(18.0, -6.0)) + noise(uv * 6.0) * 2.5 + dot(tilt, vec2(2.4, 1.8)) * 2.0 + time * 0.2);
|
||||
vec3 gold = vec3(0.84, 0.70, 0.24);
|
||||
vec3 warm = vec3(1.0, 0.88, 0.42);
|
||||
vec3 shadow = vec3(0.18, 0.14, 0.05);
|
||||
|
||||
return gold * (0.75 + 0.25 * sheen) + warm * dotMask * 0.28 + shadow * emboss * 0.08;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Standard normalized UVs [0.0 to 1.0] for macro lighting, tilt, and glare
|
||||
vec2 uv = FlutterFragCoord().xy / uSize;
|
||||
vec2 center = vec2(0.5, 0.5);
|
||||
vec2 fromCenter = uv - center;
|
||||
|
||||
// Aspect-corrected UVs for physical patterns (grid, sparkles, mosaics)
|
||||
// Dividing by the maximum dimension ensures squares remain squares.
|
||||
float maxDim = max(uSize.x, uSize.y);
|
||||
vec2 aspectUV = FlutterFragCoord().xy / maxDim;
|
||||
|
||||
float tiltMag = length(uTilt);
|
||||
float safeTiltMag = max(tiltMag, 0.001);
|
||||
vec2 tiltDir = uTilt / safeTiltMag;
|
||||
|
||||
// The highlight uses the stretched UV so the glare covers the whole widget nicely
|
||||
float highlightDistance = length(uv - (center + (uTilt * 0.35)));
|
||||
float specularMask = pow(max(0.0, 1.0 - highlightDistance * 2.6), 3.2);
|
||||
vec3 specular = vec3(specularMask) * uSpecular * (0.4 + 0.6 * tiltMag);
|
||||
|
||||
bool isCrackedIce = uStyle >= 0.5 && uStyle < 1.5;
|
||||
float sparkleGridScale = isCrackedIce ? 7.0 : 18.0;
|
||||
|
||||
// ---> USE aspectUV HERE so sparkles are drawn perfectly proportionally
|
||||
vec2 sparkleGrid = aspectUV * sparkleGridScale;
|
||||
vec2 grid = floor(sparkleGrid);
|
||||
|
||||
float rarityThreshold = isCrackedIce ? 0.965 : 0.83;
|
||||
float rarity = step(rarityThreshold, hash(grid + 0.31));
|
||||
|
||||
vec3 sparkle = vec3(0.0);
|
||||
if (rarity > 0.0) {
|
||||
vec2 cell = fract(sparkleGrid) - 0.5;
|
||||
float sparkleHash = hash(grid);
|
||||
vec2 sparkleNormal = normalize(vec2(sparkleHash * 2.0 - 1.0, hash(grid + 19.7) * 2.0 - 1.0));
|
||||
float alignment = max(0.0, dot(tiltDir, sparkleNormal));
|
||||
|
||||
if (alignment > 0.0) {
|
||||
float twinkle = 0.5 + 0.5 * sin(uTime * 5.5 + sparkleHash * TWO_PI);
|
||||
float shapeMask = 0.0;
|
||||
|
||||
if (isCrackedIce) {
|
||||
float sides = 5.0 + floor(hash(grid + 9.4) * 4.0);
|
||||
float aspect = 0.75 + hash(grid + 13.1) * 0.55;
|
||||
shapeMask = sparklePolygonMask(cell, sides, aspect, hash(grid + 5.1) * TWO_PI);
|
||||
} else {
|
||||
shapeMask = sparkleShapeMask(cell, grid);
|
||||
}
|
||||
|
||||
float sparklePower = isCrackedIce ? 4.8 : 3.2;
|
||||
float sparkleIntensity = isCrackedIce ? 0.25 : 1.0;
|
||||
float sparkleChroma = isCrackedIce ? 0.20 : 0.35;
|
||||
float sparkleMask = pow(alignment, sparklePower) * twinkle * rarity * shapeMask;
|
||||
vec3 sparkleColor = mix(vec3(1.0), rainbow(sparkleHash + dot(uTilt, vec2(0.2, -0.15)), 0.55, 1.0), sparkleChroma);
|
||||
sparkle = sparkleColor * sparkleMask * uSparkle * sparkleIntensity;
|
||||
}
|
||||
}
|
||||
|
||||
vec3 styleBase;
|
||||
// ---> USE aspectUV HERE so the foil patterns tile properly
|
||||
if (uStyle < 0.5) {
|
||||
styleBase = styleHolographicSilver(aspectUV, uTilt, uTime);
|
||||
} else if (uStyle < 1.5) {
|
||||
styleBase = styleCrackedIce(aspectUV, uTilt, uTime);
|
||||
} else if (uStyle < 2.5) {
|
||||
styleBase = styleSilverMosaic(aspectUV, uTilt, uTime);
|
||||
} else {
|
||||
styleBase = styleSuperGoldVinyl(aspectUV, uTilt, uTime);
|
||||
}
|
||||
|
||||
float styleLuma = dot(styleBase, vec3(0.299, 0.587, 0.114));
|
||||
vec3 chromaAdjusted = mix(vec3(styleLuma), styleBase, 0.2 + 0.8 * uPrismatic);
|
||||
|
||||
// General 3D lighting normal uses the stretched UV
|
||||
vec2 normal2d = normalize(fromCenter + vec2(0.001));
|
||||
float directional = 0.5 + 0.5 * dot(normal2d, tiltDir);
|
||||
float tiltLighting = mix(0.86, 1.20, pow(directional, 1.2));
|
||||
chromaAdjusted *= tiltLighting;
|
||||
|
||||
float microStrength = isCrackedIce ? 0.08 : 0.18;
|
||||
|
||||
// ---> USE aspectUV HERE so the microscopic diffraction doesn't stretch
|
||||
float microMask = 0.5 + 0.5 * sin(dot(aspectUV, vec2(137.0, 57.0)) + noise(aspectUV * 14.0) * 4.0 + dot(uTilt, vec2(9.0, 4.0)) * 2.0);
|
||||
vec3 microShimmer = rainbow(noise(aspectUV * 10.0) + dot(uTilt, vec2(0.4, -0.3)) * 0.3, 0.65, 1.0) * microMask * uDiffraction * microStrength;
|
||||
|
||||
vec3 styleColor = chromaAdjusted + microShimmer + (specular * 0.15) + sparkle;
|
||||
vec3 mappedColor = styleColor / (1.0 + styleColor * 0.3);
|
||||
float luma = dot(mappedColor, vec3(0.299, 0.587, 0.114));
|
||||
vec3 vibrantColor = mix(vec3(luma), mappedColor, 1.4);
|
||||
|
||||
float foilBrightness = dot(vibrantColor, vec3(0.333));
|
||||
float alpha = clamp(foilBrightness * 0.5, 0.0, 0.35);
|
||||
vec3 finalColor = clamp(vibrantColor, 0.0, 1.0);
|
||||
|
||||
fragColor = vec4(finalColor * alpha, alpha);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:holo_shiny/holo_shiny.dart';
|
||||
|
||||
void main() {
|
||||
test('ShinyController uses external stream when provided', () async {
|
||||
final StreamController<Offset> source =
|
||||
StreamController<Offset>.broadcast();
|
||||
final ShinyController controller =
|
||||
ShinyController(useSensor: true, tiltStream: source.stream);
|
||||
|
||||
final Future<Offset> next = controller.stream.first;
|
||||
source.add(const Offset(0.5, -0.25));
|
||||
|
||||
expect(await next, const Offset(0.5, -0.25));
|
||||
|
||||
await controller.dispose();
|
||||
await source.close();
|
||||
});
|
||||
|
||||
test('ShinyController emits nothing by default', () async {
|
||||
final ShinyController controller = ShinyController();
|
||||
final bool hasEvent = await controller.stream.isEmpty;
|
||||
expect(hasEvent, isTrue);
|
||||
await controller.dispose();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:holo_shiny/holo_shiny.dart';
|
||||
|
||||
const ValueKey<String> _cardTransformKey =
|
||||
ValueKey<String>('holo_shiny.card.transform');
|
||||
|
||||
void main() {
|
||||
test('default holograph style is crackedIce', () {
|
||||
const Shiny shiny = Shiny(child: SizedBox.shrink());
|
||||
const ShinyCard shinyCard = ShinyCard();
|
||||
|
||||
expect(shiny.style, HolographStyle.crackedIce);
|
||||
expect(shinyCard.style, HolographStyle.crackedIce);
|
||||
});
|
||||
|
||||
test('default sparkle shape is star', () {
|
||||
const Shiny shiny = Shiny(child: SizedBox.shrink());
|
||||
const ShinyCard shinyCard = ShinyCard();
|
||||
|
||||
expect(shiny.sparkleShape.kind, SparkleShapeKind.star);
|
||||
expect(shiny.sparkleShape.primary, 8.0);
|
||||
expect(shinyCard.sparkleShape.kind, SparkleShapeKind.star);
|
||||
expect(shinyCard.sparkleShape.primary, 8.0);
|
||||
});
|
||||
|
||||
test('custom sparkle factories produce expected specs', () {
|
||||
final SparkleShapeSpec star =
|
||||
SparkleShapeSpec.customStar(points: 7, innerRatio: 0.33);
|
||||
final SparkleShapeSpec polygon = SparkleShapeSpec.customPolygon(
|
||||
sides: 8, aspectRatio: 1.2, rotation: 0.4);
|
||||
final SparkleShapeSpec rectangle = SparkleShapeSpec.customRectangle(
|
||||
halfWidth: 0.20, halfHeight: 0.05, rotationJitter: 0.7);
|
||||
|
||||
expect(star.kind, SparkleShapeKind.star);
|
||||
expect(star.primary, 7.0);
|
||||
expect(star.secondary, 0.33);
|
||||
|
||||
expect(polygon.kind, SparkleShapeKind.polygon);
|
||||
expect(polygon.primary, 8.0);
|
||||
expect(polygon.secondary, 1.2);
|
||||
expect(polygon.tertiary, 0.4);
|
||||
|
||||
expect(rectangle.kind, SparkleShapeKind.rectangle);
|
||||
expect(rectangle.primary, 0.20);
|
||||
expect(rectangle.secondary, 0.05);
|
||||
expect(rectangle.tertiary, 0.7);
|
||||
});
|
||||
|
||||
testWidgets('Shiny wraps any child without card transform',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Shiny(
|
||||
enableShader: false,
|
||||
child: ColoredBox(
|
||||
color: Colors.blue,
|
||||
child: SizedBox(width: 80, height: 40),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ColoredBox), findsWidgets);
|
||||
expect(find.byKey(_cardTransformKey), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('ShinyCard renders clipping and foreground',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ShinyCard(
|
||||
enableShader: false,
|
||||
background: ColoredBox(color: Colors.black),
|
||||
foreground: Text('FOREGROUND'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('FOREGROUND'), findsOneWidget);
|
||||
expect(find.byType(ClipPath), findsOneWidget);
|
||||
expect(find.byKey(_cardTransformKey), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ShinyCard responds to external tilt stream',
|
||||
(WidgetTester tester) async {
|
||||
final StreamController<Offset> stream =
|
||||
StreamController<Offset>.broadcast();
|
||||
final ShinyController controller =
|
||||
ShinyController(tiltStream: stream.stream);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: ShinyCard(
|
||||
enableShader: false,
|
||||
controller: controller,
|
||||
background: const ColoredBox(color: Colors.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Matrix4 before =
|
||||
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
|
||||
|
||||
stream.add(const Offset(0.7, 0.2));
|
||||
await tester.pump();
|
||||
|
||||
final Matrix4 after =
|
||||
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
|
||||
|
||||
expect(after.storage[8], isNot(before.storage[8]));
|
||||
|
||||
await controller.dispose();
|
||||
await stream.close();
|
||||
});
|
||||
|
||||
testWidgets('Dragging ShinyCard updates transform',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: ShinyCard(
|
||||
enableShader: false,
|
||||
background: ColoredBox(color: Colors.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder shinyCard = find.byType(ShinyCard);
|
||||
final Matrix4 before =
|
||||
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
|
||||
|
||||
await tester.drag(shinyCard, const Offset(50, 20));
|
||||
await tester.pump();
|
||||
|
||||
final Matrix4 after =
|
||||
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
|
||||
expect(after.storage[8], isNot(before.storage[8]));
|
||||
});
|
||||
}
|
||||